mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-14 08:03:58 +00:00
Compare commits
4 Commits
fix/eval-3
...
fix/gate-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72a2cc1d8a | ||
|
|
fdee7f91f2 | ||
|
|
90691c03b9 | ||
|
|
fa07dc2a22 |
@@ -15,6 +15,9 @@ VITE_PUBLIC_SENTRY_DSN=
|
||||
# BrowserOS API URL
|
||||
VITE_PUBLIC_BROWSEROS_API=https://api.browseros.com
|
||||
|
||||
# Feature flags
|
||||
VITE_ALPHA_FEATURES=false
|
||||
|
||||
# GraphQL Schema Path (optional — falls back to schema/schema.graphql)
|
||||
GRAPHQL_SCHEMA_PATH=
|
||||
|
||||
|
||||
@@ -80,9 +80,19 @@ const primarySettingsSections: NavSection[] = [
|
||||
icon: Palette,
|
||||
feature: Feature.CUSTOMIZATION_SUPPORT,
|
||||
},
|
||||
{ name: 'Tool Approvals', to: '/settings/approvals', icon: ShieldCheck },
|
||||
{
|
||||
name: 'Tool Approvals',
|
||||
to: '/settings/approvals',
|
||||
icon: ShieldCheck,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{ name: 'BrowserOS as MCP', to: '/settings/mcp', icon: Server },
|
||||
{ name: 'ACL Rules', to: '/settings/acl', icon: ShieldAlert },
|
||||
{
|
||||
name: 'ACL Rules',
|
||||
to: '/settings/acl',
|
||||
icon: ShieldAlert,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{
|
||||
name: 'Usage & Billing',
|
||||
to: '/settings/usage',
|
||||
|
||||
@@ -41,7 +41,12 @@ const primaryNavItems: NavItem[] = [
|
||||
feature: Feature.MANAGED_MCP_SUPPORT,
|
||||
},
|
||||
{ name: 'Scheduled Tasks', to: '/scheduled', icon: CalendarClock },
|
||||
{ name: 'Agents', to: '/agents', icon: Cpu },
|
||||
{
|
||||
name: 'Agents',
|
||||
to: '/agents',
|
||||
icon: Cpu,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{
|
||||
name: 'Skills',
|
||||
to: '/home/skills',
|
||||
@@ -60,7 +65,12 @@ const primaryNavItems: NavItem[] = [
|
||||
icon: Sparkles,
|
||||
feature: Feature.SOUL_SUPPORT,
|
||||
},
|
||||
{ name: 'Governance', to: '/admin', icon: Shield },
|
||||
{
|
||||
name: 'Governance',
|
||||
to: '/admin',
|
||||
icon: Shield,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{ name: 'Settings', to: '/settings/ai', icon: Settings },
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import { HashRouter, Navigate, Route, Routes, useParams } from 'react-router'
|
||||
import { Feature } from '@/lib/browseros/capabilities'
|
||||
import { useCapabilities } from '@/lib/browseros/useCapabilities'
|
||||
import { NewTab } from '../newtab/index/NewTab'
|
||||
import { NewTabChat } from '../newtab/index/NewTabChat'
|
||||
import { NewTabLayout } from '../newtab/layout/NewTabLayout'
|
||||
import { Personalize } from '../newtab/personalize/Personalize'
|
||||
@@ -65,6 +68,8 @@ const OptionsRedirect: FC = () => {
|
||||
|
||||
export const App: FC = () => {
|
||||
const surveyParams = getSurveyParams()
|
||||
const { supports } = useCapabilities()
|
||||
const alphaEnabled = supports(Feature.ALPHA_FEATURES_SUPPORT)
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
@@ -80,16 +85,25 @@ export const App: FC = () => {
|
||||
{/* Main app with sidebar */}
|
||||
<Route element={<SidebarLayout />}>
|
||||
{/* Home routes */}
|
||||
<Route path="home" element={<NewTabLayout />}>
|
||||
<Route element={<AgentCommandLayout />}>
|
||||
<Route index element={<AgentCommandHome />} />
|
||||
<Route
|
||||
path="agents/:agentId"
|
||||
element={<AgentCommandConversation />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="chat" element={<NewTabChat />} />
|
||||
<Route path="personalize" element={<Personalize />} />
|
||||
<Route
|
||||
path="home"
|
||||
element={<NewTabLayout useChatSessionOnHome={!alphaEnabled} />}
|
||||
>
|
||||
{alphaEnabled ? (
|
||||
<>
|
||||
<Route element={<AgentCommandLayout />}>
|
||||
<Route index element={<AgentCommandHome />} />
|
||||
<Route
|
||||
path="agents/:agentId"
|
||||
element={<AgentCommandConversation />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="chat" element={<NewTabChat />} />
|
||||
<Route path="personalize" element={<Personalize />} />
|
||||
</>
|
||||
) : (
|
||||
<Route index element={<NewTab />} />
|
||||
)}
|
||||
<Route path="soul" element={<SoulPage />} />
|
||||
<Route path="skills" element={<SkillsPage />} />
|
||||
<Route path="memory" element={<MemoryPage />} />
|
||||
@@ -98,8 +112,12 @@ export const App: FC = () => {
|
||||
{/* Primary nav routes */}
|
||||
<Route path="connect-apps" element={<ConnectMCP />} />
|
||||
<Route path="scheduled" element={<ScheduledTasksPage />} />
|
||||
<Route path="agents" element={<AgentsPage />} />
|
||||
<Route path="admin" element={<AdminDashboardPage />} />
|
||||
{alphaEnabled ? (
|
||||
<Route path="agents" element={<AgentsPage />} />
|
||||
) : null}
|
||||
{alphaEnabled ? (
|
||||
<Route path="admin" element={<AdminDashboardPage />} />
|
||||
) : null}
|
||||
</Route>
|
||||
|
||||
{/* Settings with dedicated sidebar */}
|
||||
@@ -113,8 +131,12 @@ export const App: FC = () => {
|
||||
<Route path="search" element={<SearchProviderPage />} />
|
||||
<Route path="survey" element={<SurveyPage {...surveyParams} />} />
|
||||
<Route path="usage" element={<UsagePage />} />
|
||||
<Route path="acl" element={<AclSettingsPage />} />
|
||||
<Route path="approvals" element={<ToolApprovalsPage />} />
|
||||
{alphaEnabled ? (
|
||||
<>
|
||||
<Route path="acl" element={<AclSettingsPage />} />
|
||||
<Route path="approvals" element={<ToolApprovalsPage />} />
|
||||
</>
|
||||
) : null}
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -130,7 +152,12 @@ export const App: FC = () => {
|
||||
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||
<Route
|
||||
path="/personalize"
|
||||
element={<Navigate to="/home/personalize" replace />}
|
||||
element={
|
||||
<Navigate
|
||||
to={alphaEnabled ? '/home/personalize' : '/home'}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/connect-mcp"
|
||||
@@ -144,12 +171,18 @@ export const App: FC = () => {
|
||||
path="/settings/skills"
|
||||
element={<Navigate to="/home/skills" replace />}
|
||||
/>
|
||||
<Route path="/audit" element={<Navigate to="/admin" replace />} />
|
||||
<Route
|
||||
path="/audit"
|
||||
element={<Navigate to={alphaEnabled ? '/admin' : '/home'} replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/observability"
|
||||
element={<Navigate to="/admin" replace />}
|
||||
element={<Navigate to={alphaEnabled ? '/admin' : '/home'} replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/executions"
|
||||
element={<Navigate to={alphaEnabled ? '/admin' : '/home'} replace />}
|
||||
/>
|
||||
<Route path="/executions" element={<Navigate to="/admin" replace />} />
|
||||
<Route path="/options/*" element={<OptionsRedirect />} />
|
||||
|
||||
{/* Fallback to home */}
|
||||
|
||||
@@ -102,6 +102,7 @@ export const NewTab = () => {
|
||||
})
|
||||
const { selectedFolder } = useWorkspace()
|
||||
const { supports } = useCapabilities()
|
||||
const alphaEnabled = supports(Feature.ALPHA_FEATURES_SUPPORT)
|
||||
const { providers, selectedProvider, handleSelectProvider } =
|
||||
useChatSessionContext()
|
||||
const { servers: mcpServers } = useMcpServers()
|
||||
@@ -168,6 +169,8 @@ export const NewTab = () => {
|
||||
selectedTabs,
|
||||
})
|
||||
const searchPlaceholder = `Ask BrowserOS or search ${providerConfig.name}...`
|
||||
const supportsInlineChat =
|
||||
alphaEnabled && supports(Feature.NEWTAB_CHAT_SUPPORT)
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -357,7 +360,7 @@ export const NewTab = () => {
|
||||
tabs_count: selectedTabs.length,
|
||||
})
|
||||
const searchQuery = `${item.name}${item.description ? ` - ${item.description}` : ''}}`
|
||||
if (supports(Feature.NEWTAB_CHAT_SUPPORT)) {
|
||||
if (supportsInlineChat) {
|
||||
startInlineChat(searchQuery, 'agent', {
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
@@ -383,7 +386,7 @@ export const NewTab = () => {
|
||||
mode: item.mode,
|
||||
tabs_count: selectedTabs.length,
|
||||
})
|
||||
if (supports(Feature.NEWTAB_CHAT_SUPPORT)) {
|
||||
if (supportsInlineChat) {
|
||||
startInlineChat(item.message, item.mode)
|
||||
} else {
|
||||
const action = createBrowserOSAction({
|
||||
|
||||
@@ -4,10 +4,19 @@ import { ChatSessionProvider } from '@/entrypoints/sidepanel/layout/ChatSessionC
|
||||
import { NewTabFocusGrid } from './NewTabFocusGrid'
|
||||
import { shouldHideFocusGrid, shouldUseChatSession } from './route-utils'
|
||||
|
||||
export const NewTabLayout: FC = () => {
|
||||
interface NewTabLayoutProps {
|
||||
useChatSessionOnHome?: boolean
|
||||
}
|
||||
|
||||
export const NewTabLayout: FC<NewTabLayoutProps> = ({
|
||||
useChatSessionOnHome = false,
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const hideGrid = shouldHideFocusGrid(location.pathname)
|
||||
const useChatSession = shouldUseChatSession(location.pathname)
|
||||
const useChatSession = shouldUseChatSession(
|
||||
location.pathname,
|
||||
useChatSessionOnHome,
|
||||
)
|
||||
const content = (
|
||||
<>
|
||||
{!hideGrid && <NewTabFocusGrid />}
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('route-utils', () => {
|
||||
expect(isAgentConversationPath('/home')).toBe(false)
|
||||
expect(isAgentConversationPath('/home/agents/main')).toBe(true)
|
||||
expect(shouldUseChatSession('/home')).toBe(false)
|
||||
expect(shouldUseChatSession('/home', true)).toBe(true)
|
||||
expect(shouldUseChatSession('/home/agents/main')).toBe(false)
|
||||
expect(shouldUseChatSession('/home/chat')).toBe(true)
|
||||
})
|
||||
|
||||
@@ -19,6 +19,11 @@ export function shouldHideFocusGrid(pathname: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldUseChatSession(pathname: string): boolean {
|
||||
return pathname === '/home/chat'
|
||||
export function shouldUseChatSession(
|
||||
pathname: string,
|
||||
useChatSessionOnHome = false,
|
||||
): boolean {
|
||||
return (
|
||||
pathname === '/home/chat' || (useChatSessionOnHome && pathname === '/home')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { resolveStaticFeatureSupport } from './capabilities'
|
||||
|
||||
describe('resolveStaticFeatureSupport', () => {
|
||||
it('enables alpha-gated features automatically in development', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: true,
|
||||
alphaFeaturesEnabled: false,
|
||||
requiresAlphaFlag: true,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('enables alpha-gated features only when explicitly opted in', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: false,
|
||||
alphaFeaturesEnabled: true,
|
||||
requiresAlphaFlag: true,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps non-alpha features enabled in development', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: true,
|
||||
alphaFeaturesEnabled: false,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves non-alpha features unresolved in production', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: false,
|
||||
alphaFeaturesEnabled: false,
|
||||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from '../env'
|
||||
import { BrowserOSAdapter } from './adapter'
|
||||
|
||||
const SERVER_VERSION_PREF = 'browseros.server.version'
|
||||
@@ -7,16 +8,20 @@ type FeatureConfig = {
|
||||
maxBrowserOSVersion?: string
|
||||
minServerVersion?: string
|
||||
maxServerVersion?: string
|
||||
requiresAlphaFlag?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Features gated by BrowserOS version.
|
||||
* Features gated by BrowserOS version or explicit environment flags.
|
||||
* Add new features here with corresponding config in FEATURE_CONFIG.
|
||||
*
|
||||
* Note: In development mode, all features are enabled regardless of version.
|
||||
* Note: In development mode, all features are enabled regardless of version
|
||||
* or alpha flag.
|
||||
* @public
|
||||
*/
|
||||
export enum Feature {
|
||||
// Unfinished UI surfaces behind an explicit alpha opt-in
|
||||
ALPHA_FEATURES_SUPPORT = 'ALPHA_FEATURES_SUPPORT',
|
||||
// support for OpenAI-compatible provider
|
||||
OPENAI_COMPATIBLE_SUPPORT = 'OPENAI_COMPATIBLE_SUPPORT',
|
||||
// Managed MCP servers integration
|
||||
@@ -61,9 +66,11 @@ export enum Feature {
|
||||
* - maxServerVersion: feature enabled when server < this version (for deprecation)
|
||||
*
|
||||
* TypeScript enforces that every Feature has a config entry.
|
||||
* Note: In development mode, all features are enabled regardless of version.
|
||||
* In development mode, all features are enabled regardless of version or
|
||||
* alpha flag.
|
||||
*/
|
||||
const FEATURE_CONFIG: { [K in Feature]: FeatureConfig } = {
|
||||
[Feature.ALPHA_FEATURES_SUPPORT]: { requiresAlphaFlag: true },
|
||||
[Feature.OPENAI_COMPATIBLE_SUPPORT]: { minBrowserOSVersion: '0.33.0.1' },
|
||||
[Feature.MANAGED_MCP_SUPPORT]: { minBrowserOSVersion: '0.34.0.0' },
|
||||
[Feature.PERSONALIZATION_SUPPORT]: { minBrowserOSVersion: '0.36.1.0' },
|
||||
@@ -121,6 +128,24 @@ function checkVersionConstraints(
|
||||
return true
|
||||
}
|
||||
|
||||
export function resolveStaticFeatureSupport({
|
||||
isDevelopment,
|
||||
alphaFeaturesEnabled,
|
||||
requiresAlphaFlag = false,
|
||||
}: {
|
||||
isDevelopment: boolean
|
||||
alphaFeaturesEnabled: boolean
|
||||
requiresAlphaFlag?: boolean
|
||||
}): boolean | null {
|
||||
if (isDevelopment) {
|
||||
return true
|
||||
}
|
||||
if (requiresAlphaFlag) {
|
||||
return alphaFeaturesEnabled
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type CapabilitiesState = {
|
||||
browserOSVersion: number[] | null
|
||||
serverVersion: number[] | null
|
||||
@@ -128,6 +153,16 @@ type CapabilitiesState = {
|
||||
|
||||
let initPromise: Promise<CapabilitiesState> | null = null
|
||||
|
||||
function getStaticFeatureSupport(feature: Feature): boolean | null {
|
||||
const config = FEATURE_CONFIG[feature]
|
||||
if (!config) return false
|
||||
return resolveStaticFeatureSupport({
|
||||
isDevelopment: import.meta.env.DEV,
|
||||
alphaFeaturesEnabled: env.VITE_ALPHA_FEATURES,
|
||||
requiresAlphaFlag: config.requiresAlphaFlag,
|
||||
})
|
||||
}
|
||||
|
||||
async function doInitialize(): Promise<CapabilitiesState> {
|
||||
const adapter = BrowserOSAdapter.getInstance()
|
||||
const state: CapabilitiesState = {
|
||||
@@ -205,12 +240,17 @@ function checkFeatureSupport(
|
||||
* @public
|
||||
*/
|
||||
export const Capabilities = {
|
||||
getStaticSupport(feature: Feature): boolean | null {
|
||||
return getStaticFeatureSupport(feature)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a feature is supported.
|
||||
* In development mode, all features are enabled.
|
||||
*/
|
||||
async supports(feature: Feature): Promise<boolean> {
|
||||
if (import.meta.env.DEV) return true
|
||||
const staticSupport = getStaticFeatureSupport(feature)
|
||||
if (staticSupport !== null) return staticSupport
|
||||
const state = await ensureInitialized()
|
||||
return checkFeatureSupport(state, feature)
|
||||
},
|
||||
|
||||
@@ -14,6 +14,19 @@ interface UseCapabilitiesResult {
|
||||
serverVersion: string | null
|
||||
}
|
||||
|
||||
function getInitialSupportedFeatures(): Map<Feature, boolean> {
|
||||
return new Map(
|
||||
Object.values(Feature)
|
||||
.filter((value) => typeof value === 'string')
|
||||
.flatMap((feature) => {
|
||||
const supported = Capabilities.getStaticSupport(feature as Feature)
|
||||
return supported === null
|
||||
? []
|
||||
: ([[feature as Feature, supported]] as const)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for version-gated feature checks.
|
||||
* Auto-initializes Capabilities and caches feature support results.
|
||||
@@ -28,11 +41,11 @@ interface UseCapabilitiesResult {
|
||||
*/
|
||||
export function useCapabilities(): UseCapabilitiesResult {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [state, setState] = useState<CapabilitiesState>({
|
||||
const [state, setState] = useState<CapabilitiesState>(() => ({
|
||||
browserOSVersion: null,
|
||||
serverVersion: null,
|
||||
supportedFeatures: new Map(),
|
||||
})
|
||||
supportedFeatures: getInitialSupportedFeatures(),
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -72,10 +85,9 @@ export function useCapabilities(): UseCapabilitiesResult {
|
||||
|
||||
const supports = useCallback(
|
||||
(feature: Feature): boolean => {
|
||||
if (isLoading) return false
|
||||
return state.supportedFeatures.get(feature) ?? false
|
||||
},
|
||||
[isLoading, state.supportedFeatures],
|
||||
[state.supportedFeatures],
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,11 +2,15 @@ import { ZodError, z } from 'zod'
|
||||
|
||||
const EnvSchema = z.object({
|
||||
VITE_BROWSEROS_SERVER_PORT: z.coerce.number().optional(),
|
||||
VITE_ALPHA_FEATURES: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value === 'true'),
|
||||
VITE_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
VITE_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
||||
VITE_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||
VITE_PUBLIC_BROWSEROS_API: z.string().optional(),
|
||||
PROD: z.boolean(),
|
||||
PROD: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user