diff --git a/AGENTS.md b/AGENTS.md
index 1182c6ba10..432724a1e3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -93,6 +93,11 @@
- Route decoupling note: `src/components/` should stay free of direct `react-router` imports. Route state now flows through `src/context/navigation-context.tsx`, the standalone app bridges router state with `src/routes/react-router-navigation-provider.tsx`, and link-like UI should use `src/components/shared/navigation-link.tsx`.
- Test helper note: `test-utils.tsx` now wraps renders with a default `NavigationProvider` (`currentPath: "/"`, `conversationId: "test-conversation-id"`). Navigation-sensitive tests can override that via `renderWithProviders(..., { navigation: { ... } })`.
+- CSS isolation for embeddable/hosted use now relies on a scoped wrapper attribute: all bundled CSS is prefixed under `[data-agent-server-ui]` via `postcss-prefix-selector` in `vite.config.ts`, with selector exceptions handled by `transformAgentServerUISelector()` in `src/styles/agent-server-ui-style-scope.ts`. That transform must remap global selectors like `:root`, `html`, and `body` directly onto the scoped shell instead of emitting impossible descendants such as `[data-agent-server-ui] :root`.
+- Public embedding entry points should use `AgentServerUIProviders` (scoped root on by default) or `AgentServerUIRoot` for manual control. The standalone app already renders its own scoped root in `src/root.tsx`, so `src/entry.client.tsx` must pass `withStyleRoot={false}` to avoid nesting duplicate shells. Keep `AgentServerUIRoot` and the scoping constants re-exported from `src/lib/index.ts` so library consumers can customize the host wrapper without reaching into private paths.
+- Theme/customization tokens for the embedded shell are exposed as `--oh-*` CSS variables. Override them through `styleOverrides`, `style`, or host CSS targeting `[data-agent-server-ui]`; Tailwind theme tokens in `src/tailwind.css` should continue to reference those variables with `@theme inline` so host apps can restyle the UI without reworking component class names.
+- Regression coverage for the CSS isolation work lives in `__tests__/agent-server-ui-providers.test.tsx`, `__tests__/agent-server-ui-style-scope.test.ts`, and the browser-level `tests/css-isolation.spec.ts` Playwright test.
+
- Library packaging notes:
- Public npm entrypoints now come from `src/index.ts` → `src/lib/index.ts`, with domain barrels under `src/components/{conversation,terminal,browser,files,settings,sidebar}/index.ts`.
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 1b589ad2e9..bdf751eeed 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -67,6 +67,34 @@ Useful targeted verification for the isolated dev launcher:
npm run test -- __tests__/api/agent-server-config.test.ts __tests__/scripts/dev-safe.test.ts
```
+## CSS isolation and host-app customization
+
+The standalone app and the exported provider/root wrapper now scope all bundled CSS under a dedicated shell element with the `data-agent-server-ui` attribute. That means Tailwind utilities, HeroUI component styles, xterm styles, and local CSS only apply inside the OpenHands UI subtree instead of leaking into a host app.
+
+### Embedding strategy
+
+- Use `AgentServerUIProviders` in host apps. It renders a scoped style root by default.
+- For direct wrapper control, use `AgentServerUIRoot`.
+- The standalone app opts out of the provider wrapper because the router layout already renders the scoped root.
+
+### Customization strategy
+
+Theme and surface tokens are exposed as CSS custom properties on the scoped root. You can override them either through the provider/root `styleOverrides` prop or with host CSS targeting `[data-agent-server-ui]`.
+
+```tsx
+
+
+
+```
+
+If you want Tailwind layout utilities on the inner themed container, pass `contentClassName` instead of `className`, because the outer scope element is what all generated selectors key off of.
+
## Environment variables
You can create a `.env` file in the project directory with these variables based on `.env.sample`.
diff --git a/__tests__/agent-server-ui-providers.test.tsx b/__tests__/agent-server-ui-providers.test.tsx
index 1d05317446..8ee2adb9d8 100644
--- a/__tests__/agent-server-ui-providers.test.tsx
+++ b/__tests__/agent-server-ui-providers.test.tsx
@@ -11,6 +11,8 @@ vi.mock("react-i18next", async (importOriginal) =>
import OptionService from "#/api/option-service/option-service.api";
import {
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ AgentServerUIRoot,
AgentServerUIProviders,
DEFAULT_AGENT_SERVER_ANALYTICS,
OPENHANDS_I18N_NAMESPACE,
@@ -189,4 +191,70 @@ describe("AgentServerUIProviders", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
});
});
+
+ it("wraps children in a scoped, customizable style root by default", () => {
+ const { unmount } = render(
+
+ child
+ ,
+ );
+
+ const scopeRoot = document.querySelector(
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ );
+
+ expect(scopeRoot).toBeInTheDocument();
+ expect(scopeRoot?.style.getPropertyValue("--oh-color-base")).toBe(
+ "#010203",
+ );
+
+ const themedContainer =
+ scopeRoot?.firstElementChild as HTMLDivElement | null;
+ expect(themedContainer).toHaveAttribute("data-theme", "dark");
+ expect(themedContainer).toHaveClass("dark", "min-h-screen");
+ expect(themedContainer).toContainElement(
+ screen.getByTestId("styled-child"),
+ );
+
+ unmount();
+
+ render(
+
+ child
+ ,
+ );
+
+ expect(document.querySelector(AGENT_SERVER_UI_SCOPE_SELECTOR)).toBeNull();
+ });
+
+ it("exposes a standalone style root for host-controlled customization", () => {
+ render(
+
+ child
+ ,
+ );
+
+ const scopeRoot = document.querySelector(
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ );
+
+ expect(scopeRoot).toHaveClass("outer-shell");
+ expect(scopeRoot?.style.getPropertyValue("--oh-color-primary")).toBe(
+ "#abcdef",
+ );
+
+ const themedContainer =
+ scopeRoot?.firstElementChild as HTMLDivElement | null;
+ expect(themedContainer).toHaveAttribute("data-theme", "light");
+ expect(themedContainer).toHaveClass("light", "inner-shell");
+ expect(themedContainer).toContainElement(screen.getByTestId("root-child"));
+ });
});
diff --git a/__tests__/agent-server-ui-style-scope.test.ts b/__tests__/agent-server-ui-style-scope.test.ts
new file mode 100644
index 0000000000..8335ae10b7
--- /dev/null
+++ b/__tests__/agent-server-ui-style-scope.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, it } from "vitest";
+import {
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ transformAgentServerUISelector,
+} from "#/styles/agent-server-ui-style-scope";
+
+describe("transformAgentServerUISelector", () => {
+ it("prefixes ordinary selectors under the scoped root", () => {
+ expect(
+ transformAgentServerUISelector(
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ ".button-base",
+ `${AGENT_SERVER_UI_SCOPE_SELECTOR} .button-base`,
+ ),
+ ).toBe(`${AGENT_SERVER_UI_SCOPE_SELECTOR} .button-base`);
+ });
+
+ it("replaces :host selectors with the scoped root", () => {
+ expect(
+ transformAgentServerUISelector(
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ ":host",
+ `${AGENT_SERVER_UI_SCOPE_SELECTOR} :host`,
+ ),
+ ).toBe(AGENT_SERVER_UI_SCOPE_SELECTOR);
+ });
+
+ it.each([":root", "body", "html"])(
+ "maps %s selectors directly to the scoped root",
+ (selector) => {
+ expect(
+ transformAgentServerUISelector(
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ selector,
+ `${AGENT_SERVER_UI_SCOPE_SELECTOR} ${selector}`,
+ ),
+ ).toBe(AGENT_SERVER_UI_SCOPE_SELECTOR);
+ },
+ );
+
+ it("does not double-prefix selectors that are already scoped", () => {
+ const selector = `${AGENT_SERVER_UI_SCOPE_SELECTOR} .xterm`;
+
+ expect(
+ transformAgentServerUISelector(
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ selector,
+ `${AGENT_SERVER_UI_SCOPE_SELECTOR} ${selector}`,
+ ),
+ ).toBe(selector);
+ });
+});
diff --git a/__tests__/library-entrypoints.test.ts b/__tests__/library-entrypoints.test.ts
index caeaccf2f3..507594587f 100644
--- a/__tests__/library-entrypoints.test.ts
+++ b/__tests__/library-entrypoints.test.ts
@@ -19,6 +19,11 @@ describe("library public entrypoints", () => {
expect(publicApi.Sidebar).toBeTypeOf("function");
expect(publicApi.ConversationPanel).toBeTypeOf("function");
expect(publicApi.AgentServerUIProviders).toBeTypeOf("function");
+ expect(publicApi.AgentServerUIRoot).toBeTypeOf("function");
+ expect(publicApi.AGENT_SERVER_UI_SCOPE_SELECTOR).toBe(
+ "[data-agent-server-ui]",
+ );
+ expect(publicApi.AGENT_SERVER_UI_DEFAULT_THEME).toBe("dark");
});
it("keeps each component-domain barrel importable", () => {
diff --git a/global.d.ts b/global.d.ts
index c6a059caff..c75305b980 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -25,3 +25,18 @@ interface Window {
};
};
}
+
+declare module "postcss-prefix-selector" {
+ interface PrefixerOptions {
+ prefix: string;
+ transform?: (
+ prefix: string,
+ selector: string,
+ prefixedSelector: string,
+ ) => string;
+ }
+
+ export default function prefixer(
+ options: PrefixerOptions,
+ ): import("postcss").AcceptedPlugin;
+}
diff --git a/package-lock.json b/package-lock.json
index e67d46eb5c..557f4e6bd3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -85,6 +85,7 @@
"jsdom": "29.0.2",
"lint-staged": "16.4.0",
"msw": "2.13.4",
+ "postcss-prefix-selector": "2.1.1",
"prettier": "3.8.3",
"tailwindcss": "4.2.2",
"typescript": "6.0.3",
@@ -12043,6 +12044,16 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-prefix-selector": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-2.1.1.tgz",
+ "integrity": "sha512-ZBgf427Et6+XnrnJ9VXtJEKCjJwTvn2wn/qMg+wvvlRhIeFIAxdbrlZZ0CSsWYMJfcyPLBh8ogj5O1kb/Mcx3g==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
diff --git a/package.json b/package.json
index 77fbf56566..1ff47fac5f 100644
--- a/package.json
+++ b/package.json
@@ -121,6 +121,7 @@
"jsdom": "29.0.2",
"lint-staged": "16.4.0",
"msw": "2.13.4",
+ "postcss-prefix-selector": "2.1.1",
"prettier": "3.8.3",
"tailwindcss": "4.2.2",
"typescript": "6.0.3",
diff --git a/src/components/providers/agent-server-ui-providers.tsx b/src/components/providers/agent-server-ui-providers.tsx
index 76a5acb778..286a3fea67 100644
--- a/src/components/providers/agent-server-ui-providers.tsx
+++ b/src/components/providers/agent-server-ui-providers.tsx
@@ -14,6 +14,10 @@ import {
setI18n,
} from "#/i18n";
import { PostHogWrapper } from "./posthog-wrapper";
+import {
+ AgentServerUIRoot,
+ type AgentServerUIRootProps,
+} from "./agent-server-ui-root";
export type AgentServerUIAnalyticsConfig =
| {
@@ -26,11 +30,15 @@ export const DEFAULT_AGENT_SERVER_ANALYTICS: AgentServerUIAnalyticsConfig = {
provider: "posthog",
};
-export interface AgentServerUIProvidersProps {
+export interface AgentServerUIProvidersProps extends Pick<
+ AgentServerUIRootProps,
+ "className" | "contentClassName" | "style" | "styleOverrides" | "theme"
+> {
children: React.ReactNode;
queryClient?: QueryClient;
analytics?: AgentServerUIAnalyticsConfig;
i18n?: I18nInstance;
+ withStyleRoot?: boolean;
}
export function AgentServerUIProviders({
@@ -38,6 +46,12 @@ export function AgentServerUIProviders({
queryClient,
analytics,
i18n,
+ className,
+ contentClassName,
+ style,
+ styleOverrides,
+ theme,
+ withStyleRoot = true,
}: AgentServerUIProvidersProps) {
const resolvedQueryClient = React.useMemo(
() => queryClient ?? getDefaultQueryClient(),
@@ -76,10 +90,24 @@ export function AgentServerUIProviders({
children
);
+ const wrappedContent = withStyleRoot ? (
+
+ {content}
+
+ ) : (
+ content
+ );
+
return (
- {content}
+ {wrappedContent}
);
diff --git a/src/components/providers/agent-server-ui-root.tsx b/src/components/providers/agent-server-ui-root.tsx
new file mode 100644
index 0000000000..4707035da7
--- /dev/null
+++ b/src/components/providers/agent-server-ui-root.tsx
@@ -0,0 +1,53 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import React from "react";
+import { cn } from "#/utils/utils";
+import {
+ AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES,
+ AGENT_SERVER_UI_DEFAULT_THEME,
+ type AgentServerUIStyleOverrides,
+ type AgentServerUITheme,
+} from "#/styles/agent-server-ui-style-scope";
+
+export interface AgentServerUIRootProps extends Omit<
+ React.HTMLAttributes,
+ "style"
+> {
+ children: React.ReactNode;
+ theme?: AgentServerUITheme;
+ style?: React.CSSProperties;
+ styleOverrides?: AgentServerUIStyleOverrides;
+ contentClassName?: string;
+}
+
+export function AgentServerUIRoot({
+ children,
+ theme = AGENT_SERVER_UI_DEFAULT_THEME,
+ className,
+ style,
+ styleOverrides,
+ contentClassName,
+ ...divProps
+}: AgentServerUIRootProps) {
+ const scopedStyle = React.useMemo(
+ () =>
+ ({
+ ...AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES,
+ ...styleOverrides,
+ ...style,
+ }) as React.CSSProperties,
+ [style, styleOverrides],
+ );
+
+ return (
+
+ );
+}
diff --git a/src/components/providers/index.ts b/src/components/providers/index.ts
index 3fee4c1cae..d0a0835416 100644
--- a/src/components/providers/index.ts
+++ b/src/components/providers/index.ts
@@ -4,4 +4,8 @@ export {
type AgentServerUIAnalyticsConfig,
type AgentServerUIProvidersProps,
} from "./agent-server-ui-providers";
+export {
+ AgentServerUIRoot,
+ type AgentServerUIRootProps,
+} from "./agent-server-ui-root";
export { PostHogWrapper } from "./posthog-wrapper";
diff --git a/src/entry.client.tsx b/src/entry.client.tsx
index 0f157278a6..c93357ca30 100644
--- a/src/entry.client.tsx
+++ b/src/entry.client.tsx
@@ -32,7 +32,10 @@ prepareApp().then(() =>
hydrateRoot(
document,
-
+
,
diff --git a/src/index.css b/src/index.css
index 538aee842f..f67c24ec6a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,19 +1,28 @@
-@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
-@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Outfit:wght@100..900&display=swap');
+@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Outfit:wght@100..900&display=swap");
:root {
- --bg-dark: #0c0e10;
- --bg-light: #292929;
- --bg-input: #393939;
- --bg-workspace: #1f2228;
- --border: #3c3c4a;
- --text-editor-base: #9099ac;
- --text-editor-active: #c4cbda;
- --bg-editor-sidebar: #24272e;
- --bg-editor-active: #31343d;
- --border-editor-sidebar: #3c3c4a;
- background-color: var(--base) !important;
- --bg-neutral-muted: #afb8c133;
+ --oh-bg-dark: #0c0e10;
+ --oh-bg-light: #292929;
+ --oh-bg-input: #393939;
+ --oh-bg-workspace: #1f2228;
+ --oh-text-editor-base: #9099ac;
+ --oh-text-editor-active: #c4cbda;
+ --oh-bg-editor-sidebar: #24272e;
+ --oh-bg-editor-active: #31343d;
+ --oh-border-editor-sidebar: #3c3c4a;
+ --oh-bg-neutral-muted: #afb8c133;
+ --bg-dark: var(--oh-bg-dark);
+ --bg-light: var(--oh-bg-light);
+ --bg-input: var(--oh-bg-input);
+ --bg-workspace: var(--oh-bg-workspace);
+ --text-editor-base: var(--oh-text-editor-base);
+ --text-editor-active: var(--oh-text-editor-active);
+ --bg-editor-sidebar: var(--oh-bg-editor-sidebar);
+ --bg-editor-active: var(--oh-bg-editor-active);
+ --border-editor-sidebar: var(--oh-border-editor-sidebar);
+ --bg-neutral-muted: var(--oh-bg-neutral-muted);
+ background-color: var(--oh-color-base) !important;
}
body {
diff --git a/src/lib/index.ts b/src/lib/index.ts
index c462d82ccb..526dd08340 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -6,9 +6,11 @@ export * from "../components/sidebar";
export * from "../components/terminal";
export {
AgentServerUIProviders,
+ AgentServerUIRoot,
DEFAULT_AGENT_SERVER_ANALYTICS,
type AgentServerUIAnalyticsConfig,
type AgentServerUIProvidersProps,
+ type AgentServerUIRootProps,
} from "../components/providers";
export {
createAgentServerQueryClient,
@@ -27,3 +29,12 @@ export {
translationResources,
waitForI18n,
} from "../i18n";
+export {
+ AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES,
+ AGENT_SERVER_UI_DEFAULT_THEME,
+ AGENT_SERVER_UI_SCOPE_ATTRIBUTE,
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ type AgentServerUICssVariableName,
+ type AgentServerUIStyleOverrides,
+ type AgentServerUITheme,
+} from "../styles/agent-server-ui-style-scope";
diff --git a/src/root.tsx b/src/root.tsx
index e23738b87a..37c3bcf6ce 100644
--- a/src/root.tsx
+++ b/src/root.tsx
@@ -22,6 +22,7 @@ import {
import { AgentServerConnectionForm } from "#/components/features/settings/agent-server-onboarding";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useConfig } from "#/hooks/query/use-config";
+import { AgentServerUIRoot } from "#/components/providers";
export function Layout({ children }: { children: React.ReactNode }) {
return (
@@ -32,12 +33,14 @@ export function Layout({ children }: { children: React.ReactNode }) {
-
- {children}
+
+
+ {children}
+
+
+
-
-
);
diff --git a/src/styles/agent-server-ui-style-scope.ts b/src/styles/agent-server-ui-style-scope.ts
new file mode 100644
index 0000000000..de686e1254
--- /dev/null
+++ b/src/styles/agent-server-ui-style-scope.ts
@@ -0,0 +1,89 @@
+export const AGENT_SERVER_UI_SCOPE_ATTRIBUTE = "data-agent-server-ui";
+export const AGENT_SERVER_UI_SCOPE_SELECTOR = `[${AGENT_SERVER_UI_SCOPE_ATTRIBUTE}]`;
+export const AGENT_SERVER_UI_DEFAULT_THEME = "dark" as const;
+
+export type AgentServerUITheme = "dark" | "light" | "default";
+
+export const AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES = {
+ "--oh-color-primary": "#c9b974",
+ "--oh-color-logo": "#cfb755",
+ "--oh-color-base": "#0d0f11",
+ "--oh-color-base-secondary": "#24272e",
+ "--oh-color-danger": "#e76a5e",
+ "--oh-color-success": "#a5e75e",
+ "--oh-color-basic": "#9099ac",
+ "--oh-color-tertiary": "#454545",
+ "--oh-color-tertiary-light": "#b7bdc2",
+ "--oh-color-content": "#ecedee",
+ "--oh-color-content-2": "#f9fbfe",
+ "--oh-background": "#0d0f11",
+ "--oh-foreground": "#ecedee",
+ "--oh-surface": "#24272e",
+ "--oh-surface-foreground": "#ecedee",
+ "--oh-overlay": "#24272e",
+ "--oh-overlay-foreground": "#ecedee",
+ "--oh-muted": "#9099ac",
+ "--oh-scrollbar": "rgba(208, 217, 250, 0.3)",
+ "--oh-default": "#454545",
+ "--oh-default-foreground": "#ecedee",
+ "--oh-accent": "#c9b974",
+ "--oh-accent-foreground": "#0d0f11",
+ "--oh-success": "#a5e75e",
+ "--oh-success-foreground": "#0d0f11",
+ "--oh-warning": "#c9b974",
+ "--oh-warning-foreground": "#0d0f11",
+ "--oh-danger": "#e76a5e",
+ "--oh-danger-foreground": "#f9fbfe",
+ "--oh-segment": "#24272e",
+ "--oh-segment-foreground": "#ecedee",
+ "--oh-border-width": "1px",
+ "--oh-field-border-width": "1px",
+ "--oh-border": "#717888",
+ "--oh-separator": "rgba(113, 120, 136, 0.5)",
+ "--oh-focus": "#c9b974",
+ "--oh-link": "#ecedee",
+ "--oh-radius": "5px",
+ "--oh-field-radius": "5px",
+ "--oh-surface-shadow": "none",
+ "--oh-overlay-shadow": "none",
+ "--oh-field-shadow": "none",
+ "--oh-bg-dark": "#0c0e10",
+ "--oh-bg-light": "#292929",
+ "--oh-bg-input": "#393939",
+ "--oh-bg-workspace": "#1f2228",
+ "--oh-text-editor-base": "#9099ac",
+ "--oh-text-editor-active": "#c4cbda",
+ "--oh-bg-editor-sidebar": "#24272e",
+ "--oh-bg-editor-active": "#31343d",
+ "--oh-border-editor-sidebar": "#3c3c4a",
+ "--oh-bg-neutral-muted": "#afb8c133",
+} as const;
+
+export type AgentServerUICssVariableName =
+ keyof typeof AGENT_SERVER_UI_DEFAULT_CSS_VARIABLES;
+
+export type AgentServerUIStyleOverrides = Partial<
+ Record
+>;
+
+const GLOBAL_SCOPE_SELECTORS = new Set([":root", "body", "html"]);
+
+export function transformAgentServerUISelector(
+ prefix: string,
+ selector: string,
+ prefixedSelector: string,
+): string {
+ if (selector.includes(prefix)) {
+ return selector;
+ }
+
+ if (selector.includes(":host")) {
+ return selector.replaceAll(":host", prefix);
+ }
+
+ if (GLOBAL_SCOPE_SELECTORS.has(selector.trim())) {
+ return prefix;
+ }
+
+ return prefixedSelector;
+}
diff --git a/src/tailwind.css b/src/tailwind.css
index 1294eb95ee..e73cbe66e5 100644
--- a/src/tailwind.css
+++ b/src/tailwind.css
@@ -8,53 +8,95 @@
preferredstrategy: "pseudoelements";
}
-@theme {
- --color-primary: #c9b974;
- --color-logo: #cfb755;
- --color-base: #0d0f11;
- --color-base-secondary: #24272e;
- --color-danger: #e76a5e;
- --color-success: #a5e75e;
- --color-basic: #9099ac;
- --color-tertiary: #454545;
- --color-tertiary-light: #b7bdc2;
- --color-content: #ecedee;
- --color-content-2: #f9fbfe;
+@theme inline {
+ --color-primary: var(--oh-color-primary);
+ --color-logo: var(--oh-color-logo);
+ --color-base: var(--oh-color-base);
+ --color-base-secondary: var(--oh-color-base-secondary);
+ --color-danger: var(--oh-color-danger);
+ --color-success: var(--oh-color-success);
+ --color-basic: var(--oh-color-basic);
+ --color-tertiary: var(--oh-color-tertiary);
+ --color-tertiary-light: var(--oh-color-tertiary-light);
+ --color-content: var(--oh-color-content);
+ --color-content-2: var(--oh-color-content-2);
}
:root {
color-scheme: dark;
- --background: #0d0f11;
- --foreground: #ecedee;
- --surface: #24272e;
- --surface-foreground: #ecedee;
- --overlay: #24272e;
- --overlay-foreground: #ecedee;
- --muted: #9099ac;
- --scrollbar: rgba(208, 217, 250, 0.3);
- --default: #454545;
- --default-foreground: #ecedee;
- --accent: #c9b974;
- --accent-foreground: #0d0f11;
- --success: #a5e75e;
- --success-foreground: #0d0f11;
- --warning: #c9b974;
- --warning-foreground: #0d0f11;
- --danger: #e76a5e;
- --danger-foreground: #f9fbfe;
- --segment: #24272e;
- --segment-foreground: #ecedee;
- --border-width: 1px;
- --field-border-width: 1px;
- --border: #717888;
- --separator: rgba(113, 120, 136, 0.5);
- --focus: #c9b974;
- --link: #ecedee;
- --radius: 5px;
- --field-radius: 5px;
- --surface-shadow: none;
- --overlay-shadow: none;
- --field-shadow: none;
+ --oh-color-primary: #c9b974;
+ --oh-color-logo: #cfb755;
+ --oh-color-base: #0d0f11;
+ --oh-color-base-secondary: #24272e;
+ --oh-color-danger: #e76a5e;
+ --oh-color-success: #a5e75e;
+ --oh-color-basic: #9099ac;
+ --oh-color-tertiary: #454545;
+ --oh-color-tertiary-light: #b7bdc2;
+ --oh-color-content: #ecedee;
+ --oh-color-content-2: #f9fbfe;
+ --oh-background: #0d0f11;
+ --oh-foreground: #ecedee;
+ --oh-surface: #24272e;
+ --oh-surface-foreground: #ecedee;
+ --oh-overlay: #24272e;
+ --oh-overlay-foreground: #ecedee;
+ --oh-muted: #9099ac;
+ --oh-scrollbar: rgba(208, 217, 250, 0.3);
+ --oh-default: #454545;
+ --oh-default-foreground: #ecedee;
+ --oh-accent: #c9b974;
+ --oh-accent-foreground: #0d0f11;
+ --oh-success: #a5e75e;
+ --oh-success-foreground: #0d0f11;
+ --oh-warning: #c9b974;
+ --oh-warning-foreground: #0d0f11;
+ --oh-danger: #e76a5e;
+ --oh-danger-foreground: #f9fbfe;
+ --oh-segment: #24272e;
+ --oh-segment-foreground: #ecedee;
+ --oh-border-width: 1px;
+ --oh-field-border-width: 1px;
+ --oh-border: #717888;
+ --oh-separator: rgba(113, 120, 136, 0.5);
+ --oh-focus: #c9b974;
+ --oh-link: #ecedee;
+ --oh-radius: 5px;
+ --oh-field-radius: 5px;
+ --oh-surface-shadow: none;
+ --oh-overlay-shadow: none;
+ --oh-field-shadow: none;
+ --background: var(--oh-background);
+ --foreground: var(--oh-foreground);
+ --surface: var(--oh-surface);
+ --surface-foreground: var(--oh-surface-foreground);
+ --overlay: var(--oh-overlay);
+ --overlay-foreground: var(--oh-overlay-foreground);
+ --muted: var(--oh-muted);
+ --scrollbar: var(--oh-scrollbar);
+ --default: var(--oh-default);
+ --default-foreground: var(--oh-default-foreground);
+ --accent: var(--oh-accent);
+ --accent-foreground: var(--oh-accent-foreground);
+ --success: var(--oh-success);
+ --success-foreground: var(--oh-success-foreground);
+ --warning: var(--oh-warning);
+ --warning-foreground: var(--oh-warning-foreground);
+ --danger: var(--oh-danger);
+ --danger-foreground: var(--oh-danger-foreground);
+ --segment: var(--oh-segment);
+ --segment-foreground: var(--oh-segment-foreground);
+ --border-width: var(--oh-border-width);
+ --field-border-width: var(--oh-field-border-width);
+ --border: var(--oh-border);
+ --separator: var(--oh-separator);
+ --focus: var(--oh-focus);
+ --link: var(--oh-link);
+ --radius: var(--oh-radius);
+ --field-radius: var(--oh-field-radius);
+ --surface-shadow: var(--oh-surface-shadow);
+ --overlay-shadow: var(--oh-overlay-shadow);
+ --field-shadow: var(--oh-field-shadow);
}
.button-base {
@@ -377,11 +419,21 @@
}
.modal-box-shadow {
- box-shadow: 0 183px 51px 0 rgba(0, 0, 0, 0.00), 0 117px 47px 0 rgba(0, 0, 0, 0.01), 0 66px 40px 0 rgba(0, 0, 0, 0.03), 0 29px 29px 0 rgba(0, 0, 0, 0.04), 0 7px 16px 0 rgba(0, 0, 0, 0.05);
+ box-shadow:
+ 0 183px 51px 0 rgba(0, 0, 0, 0),
+ 0 117px 47px 0 rgba(0, 0, 0, 0.01),
+ 0 66px 40px 0 rgba(0, 0, 0, 0.03),
+ 0 29px 29px 0 rgba(0, 0, 0, 0.04),
+ 0 7px 16px 0 rgba(0, 0, 0, 0.05);
}
.table-box-shadow {
- box-shadow: 0 26px 7px 0 rgba(0, 0, 0, 0.00), 0 17px 7px 0 rgba(0, 0, 0, 0.01), 0 9px 6px 0 rgba(0, 0, 0, 0.03), 0 4px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ box-shadow:
+ 0 26px 7px 0 rgba(0, 0, 0, 0),
+ 0 17px 7px 0 rgba(0, 0, 0, 0.01),
+ 0 9px 6px 0 rgba(0, 0, 0, 0.03),
+ 0 4px 4px 0 rgba(0, 0, 0, 0.04),
+ 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.git-external-link-icon {
@@ -389,7 +441,12 @@
}
.api-configuration-modal {
- box-shadow: 0 183px 51px 0 rgba(0, 0, 0, 0.00), 0 117px 47px 0 rgba(0, 0, 0, 0.01), 0 66px 40px 0 rgba(0, 0, 0, 0.03), 0 29px 29px 0 rgba(0, 0, 0, 0.04), 0 7px 16px 0 rgba(0, 0, 0, 0.05);
+ box-shadow:
+ 0 183px 51px 0 rgba(0, 0, 0, 0),
+ 0 117px 47px 0 rgba(0, 0, 0, 0.01),
+ 0 66px 40px 0 rgba(0, 0, 0, 0.03),
+ 0 29px 29px 0 rgba(0, 0, 0, 0.04),
+ 0 7px 16px 0 rgba(0, 0, 0, 0.05);
}
/* Shine text effect for streaming content */
@@ -407,11 +464,22 @@
-webkit-background-clip: text !important;
background-clip: text !important;
animation: shine 2s linear infinite;
- background: radial-gradient(circle at center, rgb(24 24 27 / 85%), transparent) -200% 50% / 200% 100% no-repeat, #f4f4f5;
+ background:
+ radial-gradient(circle at center, rgb(24 24 27 / 85%), transparent) -200%
+ 50% / 200% 100% no-repeat,
+ #f4f4f5;
}
/* CTA card gradient and shadow */
.cta-card-gradient {
- background: radial-gradient(85.36% 123.38% at 50% 0%, rgba(255, 255, 255, 0.14) 0%, rgba(0, 0, 0, 0) 100%), #000000;
- box-shadow: 0px 4px 6px -4px rgba(0, 0, 0, 0.1), 0px 10px 15px -3px rgba(0, 0, 0, 0.1);
+ background:
+ radial-gradient(
+ 85.36% 123.38% at 50% 0%,
+ rgba(255, 255, 255, 0.14) 0%,
+ rgba(0, 0, 0, 0) 100%
+ ),
+ #000000;
+ box-shadow:
+ 0px 4px 6px -4px rgba(0, 0, 0, 0.1),
+ 0px 10px 15px -3px rgba(0, 0, 0, 0.1);
}
diff --git a/tests/css-isolation.spec.ts b/tests/css-isolation.spec.ts
new file mode 100644
index 0000000000..61b1343a68
--- /dev/null
+++ b/tests/css-isolation.spec.ts
@@ -0,0 +1,32 @@
+import { expect, test } from "@playwright/test";
+
+test("scopes standalone styles to the agent server UI shell", async ({
+ page,
+}) => {
+ await page.goto("/");
+
+ await expect(page.locator("[data-agent-server-ui]")).toBeVisible();
+ const layout = page.getByTestId("root-layout");
+ await expect(layout).toBeVisible();
+
+ const insideBackground = await layout.evaluate(
+ (element) => getComputedStyle(element).backgroundColor,
+ );
+
+ const outsideStyles = await page.evaluate(() => {
+ const hostProbe = document.createElement("div");
+ hostProbe.className = "bg-base text-content-2";
+ hostProbe.textContent = "host";
+ document.body.appendChild(hostProbe);
+
+ const styles = getComputedStyle(hostProbe);
+
+ return {
+ backgroundColor: styles.backgroundColor,
+ color: styles.color,
+ };
+ });
+
+ expect(insideBackground).not.toBe("rgba(0, 0, 0, 0)");
+ expect(outsideStyles.backgroundColor).not.toBe(insideBackground);
+});
diff --git a/vite.config.ts b/vite.config.ts
index e269b5dfeb..67c372c9b5 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,6 +7,11 @@ import svgr from "vite-plugin-svgr";
import { reactRouter } from "@react-router/dev/vite";
import { configDefaults } from "vitest/config";
import tailwindcss from "@tailwindcss/vite";
+import prefixer from "postcss-prefix-selector";
+import {
+ AGENT_SERVER_UI_SCOPE_SELECTOR,
+ transformAgentServerUISelector,
+} from "./src/styles/agent-server-ui-style-scope";
const LIB_ENTRY = fileURLToPath(new URL("./src/index.ts", import.meta.url));
const LIB_EXTERNALS = [
@@ -42,6 +47,26 @@ export default defineConfig(({ mode }) => {
svgr(),
tailwindcss(),
],
+ css: {
+ postcss: {
+ plugins: [
+ prefixer({
+ prefix: AGENT_SERVER_UI_SCOPE_SELECTOR,
+ transform(
+ prefix: string,
+ selector: string,
+ prefixedSelector: string,
+ ) {
+ return transformAgentServerUISelector(
+ prefix,
+ selector,
+ prefixedSelector,
+ );
+ },
+ }),
+ ],
+ },
+ },
build: isLibraryBuild
? {
outDir: "dist",