diff --git a/web/.prettierignore b/web/.prettierignore
index eb8ce09a8d..33f39170b3 100644
--- a/web/.prettierignore
+++ b/web/.prettierignore
@@ -3,3 +3,7 @@ helm/
# Lock file
pnpm-lock.yaml
+
+# Build output
+dist/
+packages/*/dist/
diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.spec.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.spec.tsx
new file mode 100644
index 0000000000..068d1ca7b7
--- /dev/null
+++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.spec.tsx
@@ -0,0 +1,66 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { ROUTES } from '@studio/constants/routes';
+import { DashboardLandingRoute } from '@studio/routes/DashboardLandingRoute';
+import { TestProviders } from '@studio/tests/util/TestProviders';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { createMemoryRouter, generatePath, RouterProvider } from 'react-router';
+
+const workspace = 'default';
+
+const renderRoute = () => {
+ const route = generatePath(ROUTES.workspace.dashboard, { workspace });
+ const router = createMemoryRouter(
+ [{ path: ROUTES.workspace.dashboard, element: }],
+ {
+ initialEntries: [route],
+ }
+ );
+
+ return render(
+
+
+
+ );
+};
+
+describe('DashboardLandingRoute', () => {
+ it('renders the dashboard landing page', async () => {
+ renderRoute();
+
+ expect(await screen.findByText('What would you like to do?')).toBeInTheDocument();
+ expect(screen.getByRole('textbox', { name: 'Message Claude' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Explore repo/ })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Draft a change/ })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Review recent work/ })).toBeInTheDocument();
+ });
+
+ it('lets prompt suggestions populate the landing composer', async () => {
+ const user = userEvent.setup();
+ renderRoute();
+
+ await user.click(await screen.findByRole('button', { name: /Explore repo/ }));
+
+ expect(screen.getByRole('textbox', { name: 'Message Claude' })).toHaveValue(
+ 'Give me a concise map of this repo and the main places I should know about.'
+ );
+ });
+
+ it('only enables the send affordance once the composer has text', async () => {
+ const user = userEvent.setup();
+ renderRoute();
+
+ const composer = await screen.findByRole('textbox', { name: 'Message Claude' });
+ const sendButton = screen.getByRole('button', { name: 'Send message' });
+
+ expect(sendButton).toBeDisabled();
+
+ await user.type(composer, 'Sketch a dashboard');
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: 'Send message' })).toBeEnabled();
+ });
+ });
+});
diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx
new file mode 100644
index 0000000000..eb26f83cc3
--- /dev/null
+++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx
@@ -0,0 +1,145 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { Button, Card, Flex, Text, TextArea, Tooltip } from '@nvidia/foundations-react-core';
+import { AccessibleTitle } from '@studio/components/AccessibleTitle';
+import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs';
+import { GitBranch, Hammer, Search, Send, Terminal } from 'lucide-react';
+import {
+ type ChangeEvent,
+ type FC,
+ type FormEvent,
+ type ReactNode,
+ useCallback,
+ useState,
+} from 'react';
+
+interface PromptSuggestion {
+ title: string;
+ prompt: string;
+ icon: ReactNode;
+}
+
+const PROMPT_SUGGESTIONS: PromptSuggestion[] = [
+ {
+ title: 'Explore repo',
+ prompt: 'Give me a concise map of this repo and the main places I should know about.',
+ icon: ,
+ },
+ {
+ title: 'Draft a change',
+ prompt: 'Help me plan and implement the next small product improvement in nemo-platform.',
+ icon: ,
+ },
+ {
+ title: 'Review recent work',
+ prompt: 'Review the current working tree and call out anything risky or unfinished.',
+ icon: ,
+ },
+];
+
+const PromptCard = ({
+ suggestion,
+ onSelect,
+}: {
+ suggestion: PromptSuggestion;
+ onSelect: () => void;
+}) => (
+
+
+
+);
+
+const LandingComposer = ({
+ input,
+ onChange,
+}: {
+ input: string;
+ onChange: (value: string) => void;
+}) => {
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ };
+
+ return (
+
+ );
+};
+
+export const DashboardLandingRoute: FC = () => {
+ const [input, setInput] = useState('');
+
+ useBreadcrumbs({
+ items: [{ slotLabel: 'Dashboard' }],
+ });
+
+ const handlePromptSelect = useCallback((prompt: string) => {
+ setInput(prompt);
+ }, []);
+
+ return (
+
+
+
+
+
+ What would you like to do?
+
+
+
+
+
+
+ {PROMPT_SUGGESTIONS.map((suggestion) => (
+ handlePromptSelect(suggestion.prompt)}
+ />
+ ))}
+
+
+
+
+ );
+};
diff --git a/web/packages/studio/src/routes/RootRedirect/index.spec.tsx b/web/packages/studio/src/routes/RootRedirect/index.spec.tsx
new file mode 100644
index 0000000000..aa04b83014
--- /dev/null
+++ b/web/packages/studio/src/routes/RootRedirect/index.spec.tsx
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { ROUTES } from '@studio/constants/routes';
+import { LOCATION_DISPLAY_TEST_ID } from '@studio/tests/util/constants';
+import { LocationDisplay } from '@studio/tests/util/LocationDisplay';
+import { TestProviders } from '@studio/tests/util/TestProviders';
+import { render, screen } from '@testing-library/react';
+import { createMemoryRouter, RouterProvider } from 'react-router';
+
+const renderRootRedirect = async (initialPath = '/') => {
+ const { RootRedirect } = await import('@studio/routes/RootRedirect');
+ const router = createMemoryRouter(
+ [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/workspaces',
+ element: ,
+ },
+ {
+ path: ROUTES.workspace.dashboard,
+ element: ,
+ },
+ {
+ path: ROUTES.workspace.agentsList,
+ element: ,
+ },
+ {
+ path: ROUTES.workspace.index,
+ element: ,
+ },
+ ],
+ { initialEntries: [initialPath] }
+ );
+
+ return render(
+
+
+
+ );
+};
+
+describe('RootRedirect', () => {
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ });
+
+ it('uses coding agent studio as the root landing page when enabled', async () => {
+ vi.resetModules();
+ vi.stubEnv('VITE_FF_CODING_AGENT_STUDIO_ENABLED', 'true');
+
+ await renderRootRedirect();
+
+ const location = await screen.findByTestId(LOCATION_DISPLAY_TEST_ID);
+ expect(location).toHaveTextContent('/workspaces/');
+ expect(location).toHaveTextContent('/dashboard');
+ });
+
+ it('uses the dashboard route as the /workspaces landing page when coding agent studio is enabled', async () => {
+ vi.resetModules();
+ vi.stubEnv('VITE_FF_CODING_AGENT_STUDIO_ENABLED', 'true');
+ await renderRootRedirect('/workspaces');
+
+ const location = await screen.findByTestId(LOCATION_DISPLAY_TEST_ID);
+ expect(location).toHaveTextContent('/workspaces/');
+ expect(location).toHaveTextContent('/dashboard');
+ });
+});
diff --git a/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx b/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx
index 2387e55768..2798655237 100644
--- a/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx
+++ b/web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx
@@ -6,6 +6,7 @@ import { NavigationDrawer } from '@studio/components/Layouts/NavigationDrawer';
import {
AGENTS_ENABLED,
BASE_MODELS_ENABLED,
+ CODING_AGENT_STUDIO_ENABLED,
CUSTOMIZER_ENABLED,
DASHBOARD_ENABLED,
DATA_DESIGNER_ENABLED,
@@ -68,16 +69,17 @@ export const WorkspaceSideNav = ({ collapsed }: { collapsed?: boolean }) => {
const workspace = useWorkspaceFromPath();
const items = useMemo(() => {
- const dashboardNav = DASHBOARD_ENABLED
- ? [
- {
- id: 'dashboard',
- slotIcon: ,
- slotLabel: 'Dashboard',
- href: getWorkspaceDashboardRoute(workspace),
- },
- ]
- : [];
+ const dashboardNav =
+ DASHBOARD_ENABLED || CODING_AGENT_STUDIO_ENABLED
+ ? [
+ {
+ id: 'dashboard',
+ slotIcon: ,
+ slotLabel: 'Dashboard',
+ href: getWorkspaceDashboardRoute(workspace),
+ },
+ ]
+ : [];
const jobsNav = JOBS_ENABLED
? [
{
diff --git a/web/packages/studio/src/routes/index.spec.tsx b/web/packages/studio/src/routes/index.spec.tsx
index ec83338561..0c2cba8a33 100644
--- a/web/packages/studio/src/routes/index.spec.tsx
+++ b/web/packages/studio/src/routes/index.spec.tsx
@@ -165,6 +165,13 @@ describe('Routes', () => {
).toBe(true);
});
+ it('should include the dashboard route if coding agent studio is enabled', async () => {
+ vi.stubEnv('VITE_FF_CODING_AGENT_STUDIO_ENABLED', 'true');
+ vi.stubEnv('VITE_FF_DASHBOARD_ENABLED', 'false');
+ const { routes } = await import('./index');
+ expect(findIfRouteExists(routes, ROUTES.workspace.dashboard)).toBe(true);
+ });
+
it('should exclude safe synthesizer routes if safe synthesizer is disabled', async () => {
vi.stubEnv('VITE_FF_SAFE_SYNTHESIZER_ENABLED', 'false');
const { routes } = await import('./index');
diff --git a/web/packages/studio/src/routes/index.tsx b/web/packages/studio/src/routes/index.tsx
index acdcce8cdb..77cd69dd0d 100644
--- a/web/packages/studio/src/routes/index.tsx
+++ b/web/packages/studio/src/routes/index.tsx
@@ -7,6 +7,7 @@ import { ErrorPanel } from '@studio/components/ErrorPanel';
import { Loading } from '@studio/components/Layouts/Loading';
import {
AGENTS_ENABLED,
+ CODING_AGENT_STUDIO_ENABLED,
DATA_DESIGNER_ENABLED,
DEPLOYMENTS_ENABLED,
GUARDRAILS_ENABLED,
@@ -170,6 +171,11 @@ const PromptTuningFormRoute = lazy(() =>
default: module.PromptTuningFormRoute,
}))
);
+const DashboardLandingRoute = lazy(() =>
+ import('@studio/routes/DashboardLandingRoute').then((module) => ({
+ default: module.DashboardLandingRoute,
+ }))
+);
const ModelCompareRoute =
MODEL_COMPARE_ENABLED &&
lazy(() =>
@@ -391,7 +397,13 @@ export const routes: RouteObject[] = [
...gateDashboardRoutes([
{
path: ROUTES.workspace.dashboard,
- element: ,
+ element: CODING_AGENT_STUDIO_ENABLED ? (
+ }>
+
+
+ ) : (
+
+ ),
errorElement: ,
},
]),
diff --git a/web/packages/studio/src/routes/utils.ts b/web/packages/studio/src/routes/utils.ts
index af52f2ff98..6071ea9ac2 100644
--- a/web/packages/studio/src/routes/utils.ts
+++ b/web/packages/studio/src/routes/utils.ts
@@ -5,6 +5,7 @@ import { getPartsFromNamedEntityRef, NamedEntityRef } from '@nemo/common/src/nam
import {
AGENTS_ENABLED,
BASE_MODELS_ENABLED,
+ CODING_AGENT_STUDIO_ENABLED,
CUSTOMIZER_ENABLED,
DASHBOARD_ENABLED,
DATA_DESIGNER_ENABLED,
@@ -41,7 +42,7 @@ export const gateCustomizationRoutes = (routes: RouteObject | RouteObject[]) =>
gateRoutes(CUSTOMIZER_ENABLED, routes);
export const gateDashboardRoutes = (routes: RouteObject | RouteObject[]) =>
- gateRoutes(DASHBOARD_ENABLED, routes);
+ gateRoutes(DASHBOARD_ENABLED || CODING_AGENT_STUDIO_ENABLED, routes);
export const gateDatasetsRoutes = (routes: RouteObject | RouteObject[]) =>
gateRoutes(DATASETS_ENABLED, routes);
@@ -126,7 +127,8 @@ export const getWorkspaceIndexRoute = (workspace: string) => {
};
export const getWorkspaceDetailsDefaultRoute = (workspace: string) => {
- if (DASHBOARD_ENABLED) return getWorkspaceDashboardRoute(workspace);
+ if (DASHBOARD_ENABLED || CODING_AGENT_STUDIO_ENABLED)
+ return getWorkspaceDashboardRoute(workspace);
if (AGENTS_ENABLED) return getAgentsListRoute(workspace);
if (BASE_MODELS_ENABLED) return getWorkspaceBaseModelsRoute(workspace);
if (JOBS_ENABLED) return getWorkspaceJobsRoute(workspace);