From 7c8be9cb9f1134c4b4b06158df7f6fdd96457f71 Mon Sep 17 00:00:00 2001
From: Dilan Induwara <153802063+Induwara04@users.noreply.github.com>
Date: Tue, 11 Aug 2026 15:42:35 +0530
Subject: [PATCH 1/2] Add Pluggable capability to AI workspace UI
---
portals/ai-workspace/package.json | 2 +
portals/ai-workspace/src/AIWorkspace.tsx | 131 ++++++++++++++++++
portals/ai-workspace/src/App.tsx | 26 +++-
portals/ai-workspace/src/extensions.tsx | 34 +++++
portals/ai-workspace/src/index.ts | 11 ++
portals/ai-workspace/src/main.tsx | 118 +---------------
.../src/pages/appShell/AppSidebar.tsx | 26 ++++
.../src/pages/appShell/appShellMain.tsx | 14 +-
8 files changed, 249 insertions(+), 113 deletions(-)
create mode 100644 portals/ai-workspace/src/AIWorkspace.tsx
create mode 100644 portals/ai-workspace/src/extensions.tsx
create mode 100644 portals/ai-workspace/src/index.ts
diff --git a/portals/ai-workspace/package.json b/portals/ai-workspace/package.json
index 0827f89cfe..2a87591da0 100644
--- a/portals/ai-workspace/package.json
+++ b/portals/ai-workspace/package.json
@@ -3,6 +3,8 @@
"version": "0.0.0",
"private": true,
"type": "module",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
"scripts": {
"dev": "vite",
"start": "vite",
diff --git a/portals/ai-workspace/src/AIWorkspace.tsx b/portals/ai-workspace/src/AIWorkspace.tsx
new file mode 100644
index 0000000000..a9cfed8838
--- /dev/null
+++ b/portals/ai-workspace/src/AIWorkspace.tsx
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ * Licensed under the Apache License, Version 2.0.
+ */
+
+import React, { useEffect } from "react";
+import { BrowserRouter } from "react-router-dom";
+import { IntlProvider } from "react-intl";
+import {
+ AcrylicOrangeTheme,
+ AcrylicPurpleTheme,
+ ClassicTheme,
+ HighContrastTheme,
+ OxygenUIThemeProvider,
+ Box,
+ LinearProgress,
+ Stack,
+ Typography,
+} from "@wso2/oxygen-ui";
+
+import App from "./App";
+import "./styles.css";
+import { AUTH_MODE } from "./config.env";
+import { BASE_PATH } from "./paths";
+import { BFFAuthProvider } from "./contexts/BFFAuthProvider";
+import { useAppAuth } from "./contexts/AppAuthContext";
+import BasicAuthLoginPage from "./pages/login/BasicAuthLoginPage";
+import type { AIWorkspaceExtension } from "./extensions";
+
+function LoadingScreen({ message }: { message?: string }) {
+ return (
+
+
+ {message && {message}}
+
+
+
+
+
+ );
+}
+
+function OIDCRedirect() {
+ const { login } = useAppAuth();
+ useEffect(() => {
+ void login();
+ }, [login]);
+ return ;
+}
+
+function AppGate({
+ extensions,
+}: {
+ extensions: readonly AIWorkspaceExtension[];
+}) {
+ const { isAuthenticated, isLoading } = useAppAuth();
+ if (isLoading) return ;
+ if (!isAuthenticated) {
+ if (AUTH_MODE === "basic") {
+ return (
+ {
+ const { pathname, search, hash } = window.location;
+ const route = pathname.startsWith(BASE_PATH)
+ ? pathname.slice(BASE_PATH.length)
+ : pathname;
+ window.location.replace(
+ route === "/login" || route === "/signin"
+ ? `${BASE_PATH}/`
+ : pathname + search + hash,
+ );
+ }}
+ />
+ );
+ }
+ return ;
+ }
+ return (
+
+
+
+ );
+}
+
+export type AIWorkspaceProps = {
+ extensions?: readonly AIWorkspaceExtension[];
+};
+
+export default function AIWorkspace({ extensions = [] }: AIWorkspaceProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/portals/ai-workspace/src/App.tsx b/portals/ai-workspace/src/App.tsx
index 048ad54ca0..9a00c71e28 100644
--- a/portals/ai-workspace/src/App.tsx
+++ b/portals/ai-workspace/src/App.tsx
@@ -85,6 +85,10 @@ import { ChoreoUserProvider } from './contexts/ChoreoUserContext';
import { useAppAuth } from './contexts/AppAuthContext';
import { Box, Button, Stack, Typography } from '@wso2/oxygen-ui';
import OoopsImage from './assets/images/Ooops.svg';
+import {
+ ExtensionsProvider,
+ type AIWorkspaceExtension,
+} from './extensions';
/**
* Only allow same-origin relative paths as return URLs to prevent open redirects.
@@ -275,7 +279,17 @@ function WithPageBoundary({ children }: { children: React.ReactNode }) {
return {children};
}
-export default function App() {
+export type AppProps = {
+ extensions?: readonly AIWorkspaceExtension[];
+};
+
+function WorkspaceRoutes({ extensions = [] }: AppProps) {
+ const extensionRoutes = extensions.map((extension) => (
+ {extension.element}
+ } />
+ ));
+
return (
@@ -592,6 +606,7 @@ export default function App() {
/>
+ {extensionRoutes}
}>
} />
+ {extensionRoutes}
@@ -812,3 +828,11 @@ export default function App() {
);
}
+
+export default function App({ extensions = [] }: AppProps) {
+ return (
+
+
+
+ );
+}
diff --git a/portals/ai-workspace/src/extensions.tsx b/portals/ai-workspace/src/extensions.tsx
new file mode 100644
index 0000000000..f0760fcc4f
--- /dev/null
+++ b/portals/ai-workspace/src/extensions.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ * Licensed under the Apache License, Version 2.0.
+ */
+
+import React, { createContext, useContext } from 'react';
+
+export type AIWorkspaceExtension = {
+ id: string;
+ path: string;
+ label: string;
+ icon?: React.ReactNode;
+ element: React.ReactNode;
+};
+
+const ExtensionsContext = createContext([]);
+
+export function ExtensionsProvider({
+ extensions,
+ children,
+}: {
+ extensions: readonly AIWorkspaceExtension[];
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAIWorkspaceExtensions(): readonly AIWorkspaceExtension[] {
+ return useContext(ExtensionsContext);
+}
diff --git a/portals/ai-workspace/src/index.ts b/portals/ai-workspace/src/index.ts
new file mode 100644
index 0000000000..12a53c9802
--- /dev/null
+++ b/portals/ai-workspace/src/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ * Licensed under the Apache License, Version 2.0.
+ */
+
+export { default as App } from './App';
+export type { AppProps } from './App';
+export { default as AIWorkspace } from './AIWorkspace';
+export type { AIWorkspaceProps } from './AIWorkspace';
+export type { AIWorkspaceExtension } from './extensions';
+export { BASE_PATH } from './paths';
diff --git a/portals/ai-workspace/src/main.tsx b/portals/ai-workspace/src/main.tsx
index 1317a16940..d176b360e0 100644
--- a/portals/ai-workspace/src/main.tsx
+++ b/portals/ai-workspace/src/main.tsx
@@ -1,118 +1,14 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * Licensed under the Apache License, Version 2.0.
*/
-import React, { useEffect } from 'react';
-import { createRoot } from 'react-dom/client';
-import { BrowserRouter } from 'react-router-dom';
-import { IntlProvider } from 'react-intl';
-import {
- AcrylicOrangeTheme, AcrylicPurpleTheme, ClassicTheme,
- HighContrastTheme, OxygenUIThemeProvider,
- Box, LinearProgress, Stack, Typography,
-} from '@wso2/oxygen-ui';
+import React from "react";
+import { createRoot } from "react-dom/client";
+import AIWorkspace from "./AIWorkspace";
-import App from './App.tsx';
-import './styles.css';
-import { AUTH_MODE } from './config.env';
-import { BASE_PATH } from './paths';
-import { BFFAuthProvider } from './contexts/BFFAuthProvider';
-import { useAppAuth } from './contexts/AppAuthContext';
-import BasicAuthLoginPage from './pages/login/BasicAuthLoginPage';
-
-// ── Loading screen ──────────────────────────────────────────────────────────
-
-function LoadingScreen({ message }: { message?: string }) {
- return (
-
-
- {message && {message}}
-
-
-
-
-
- );
-}
-
-// ── OIDC redirect — the BFF owns the handshake; we just navigate to it ─────────
-
-function OIDCRedirect() {
- const { login } = useAppAuth();
- useEffect(() => { void login(); }, [login]);
- return ;
-}
-
-// ── AppGate — decides login UX vs. the authenticated app ──────────────────────
-
-function AppGate() {
- const { isAuthenticated, isLoading } = useAppAuth();
-
- if (isLoading) {
- return ;
- }
-
- if (!isAuthenticated) {
- if (AUTH_MODE === 'basic') {
- // On success the BFF has set the session cookie; reload to re-hydrate while
- // preserving the path the user originally requested (matching OIDC return
- // behaviour). Only avoid pinning to the login route itself.
- return {
- // window.location is outside the router, so these paths carry the base path
- // (BrowserRouter's basename) and the fallback has to add it back.
- const { pathname, search, hash } = window.location;
- const route = pathname.startsWith(BASE_PATH) ? pathname.slice(BASE_PATH.length) : pathname;
- const target = route === '/login' || route === '/signin'
- ? `${BASE_PATH}/`
- : pathname + search + hash;
- window.location.replace(target);
- }} />;
- }
- return ;
- }
-
- return (
-
-
-
- );
-}
-
-// ── Entry point ───────────────────────────────────────────────────────────────
-
-const container = document.getElementById('root')!;
-const root = createRoot(container);
-
-root.render(
+createRoot(document.getElementById("root")!).render(
-
-
-
-
-
-
-
-
+
+ ,
);
diff --git a/portals/ai-workspace/src/pages/appShell/AppSidebar.tsx b/portals/ai-workspace/src/pages/appShell/AppSidebar.tsx
index 35d3091865..0946056ab0 100644
--- a/portals/ai-workspace/src/pages/appShell/AppSidebar.tsx
+++ b/portals/ai-workspace/src/pages/appShell/AppSidebar.tsx
@@ -38,6 +38,7 @@ import { buildOrgPath, buildProjectPath } from '../../utils/projectRouting';
import QuickStartIntroPopup, {
QS_INTRO_STORAGE_KEY,
} from './QuickStartIntroPopup';
+import { useAIWorkspaceExtensions } from '../../extensions';
const navLinkStyle: React.CSSProperties = {
textDecoration: 'none',
@@ -64,6 +65,7 @@ export default function AppSidebar({
const [showIntro, setShowIntro] = useState(
() => !localStorage.getItem(QS_INTRO_STORAGE_KEY)
);
+ const extensions = useAIWorkspaceExtensions();
const handleDismissIntro = () => {
localStorage.setItem(QS_INTRO_STORAGE_KEY, '1');
@@ -127,6 +129,9 @@ export default function AppSidebar({
const settingsPath = currentProject
? buildProjectPath(currentOrganization, currentProject, '/settings')
: orgSettingsPath;
+ const extensionPath = (path: string) => currentProject
+ ? buildProjectPath(currentOrganization, currentProject, `/${path}`)
+ : buildOrgPath(currentOrganization, `/${path}`);
return (
)}
+ {extensions.length > 0 && (
+
+
+ Cloud
+
+ {extensions.map((extension) => (
+
+
+
+ {extension.icon ?? }
+
+ {extension.label}
+
+
+ ))}
+
+ )}
diff --git a/portals/ai-workspace/src/pages/appShell/appShellMain.tsx b/portals/ai-workspace/src/pages/appShell/appShellMain.tsx
index d5403b454c..3be1217dec 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellMain.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellMain.tsx
@@ -48,6 +48,7 @@ import {
import { logger } from '../../utils/logger';
import { FormattedMessage } from 'react-intl';
import OoopsImage from '../../assets/images/Ooops.svg';
+import { useAIWorkspaceExtensions } from '../../extensions';
type SelectableOrg = {
id: string;
@@ -64,6 +65,7 @@ type SelectableProject = {
export default function AppLayout(): JSX.Element {
const navigate = useNavigate();
const location = useLocation();
+ const extensions = useAIWorkspaceExtensions();
const { logout } = useAppAuth();
// [standalone] const { signOut } = useAuthContext();
@@ -142,6 +144,16 @@ export default function AppLayout(): JSX.Element {
? segments[3] ?? ''
: segments[1] ?? '';
const tertiarySegment = isOrgScoped ? segments[4] ?? '' : segments[2] ?? '';
+ const extensionSegment = primarySegment === 'projects'
+ ? tertiarySegment
+ : primarySegment;
+ const activeExtension = extensions.find(
+ (extension) => extension.path.split('/')[0] === extensionSegment
+ );
+ if (activeExtension) {
+ shellActions.setActiveMenuItem(activeExtension.id);
+ return;
+ }
if (!primarySegment || primarySegment === 'home') {
shellActions.setActiveMenuItem('overview');
@@ -240,7 +252,7 @@ export default function AppLayout(): JSX.Element {
if (primarySegment === 'settings') {
shellActions.setActiveMenuItem('settings');
}
- }, [location.pathname, shellActions, currentProject, currentOrganization]);
+ }, [location.pathname, shellActions, currentProject, currentOrganization, extensions]);
useEffect(() => {
const legalLinks = [
From 75ade98bda3326900597155bbace4630065a2f0e Mon Sep 17 00:00:00 2001
From: Dilan Induwara <153802063+Induwara04@users.noreply.github.com>
Date: Fri, 14 Aug 2026 12:09:15 +0530
Subject: [PATCH 2/2] Add licenses header
---
portals/ai-workspace/src/AIWorkspace.tsx | 15 ++++++++++++++-
portals/ai-workspace/src/extensions.tsx | 15 ++++++++++++++-
portals/ai-workspace/src/main.tsx | 15 ++++++++++++++-
3 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/portals/ai-workspace/src/AIWorkspace.tsx b/portals/ai-workspace/src/AIWorkspace.tsx
index a9cfed8838..a86fd5c56a 100644
--- a/portals/ai-workspace/src/AIWorkspace.tsx
+++ b/portals/ai-workspace/src/AIWorkspace.tsx
@@ -1,6 +1,19 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- * Licensed under the Apache License, Version 2.0.
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React, { useEffect } from "react";
diff --git a/portals/ai-workspace/src/extensions.tsx b/portals/ai-workspace/src/extensions.tsx
index f0760fcc4f..e7e7ccbbe3 100644
--- a/portals/ai-workspace/src/extensions.tsx
+++ b/portals/ai-workspace/src/extensions.tsx
@@ -1,6 +1,19 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- * Licensed under the Apache License, Version 2.0.
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React, { createContext, useContext } from 'react';
diff --git a/portals/ai-workspace/src/main.tsx b/portals/ai-workspace/src/main.tsx
index d176b360e0..7439da6f83 100644
--- a/portals/ai-workspace/src/main.tsx
+++ b/portals/ai-workspace/src/main.tsx
@@ -1,6 +1,19 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
- * Licensed under the Apache License, Version 2.0.
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
*/
import React from "react";