} />
);
// Wait for Suspense / lazy components to settle before the test proceeds
- await screen.findByRole('button', { name: /(Collapse|Expand) sidebar/i });
+ await screen.findAllByRole('button', { name: /(Collapse|Expand) sidebar/i });
};
describe('GlobalNav', () => {
@@ -172,4 +177,34 @@ describe('GlobalNav', () => {
expect(sideNavSpy).toHaveBeenCalledWith(false);
});
});
+
+ describe('Code Agent top bar chat', () => {
+ it('mounts outside the dashboard and full Code Agent routes', async () => {
+ createMatchMediaMock(true);
+
+ await renderGlobalNav('/workspaces/test-workspace/jobs');
+
+ expect(screen.getByTestId('code-agent-top-bar-chat')).toBeInTheDocument();
+ });
+
+ it('does not mount on the dashboard route', async () => {
+ createMatchMediaMock(true);
+
+ await renderGlobalNav(
+ generatePath(ROUTES.workspace.dashboard, { workspace: 'test-workspace' })
+ );
+
+ expect(screen.queryByTestId('code-agent-top-bar-chat')).not.toBeInTheDocument();
+ });
+
+ it('does not mount on the full Code Agent route', async () => {
+ createMatchMediaMock(true);
+
+ await renderGlobalNav(
+ generatePath(ROUTES.workspace.claudeCodeChat, { workspace: 'test-workspace' })
+ );
+
+ expect(screen.queryByTestId('code-agent-top-bar-chat')).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx b/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx
index 94a668da0c..5885cb32c6 100644
--- a/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx
+++ b/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx
@@ -5,13 +5,15 @@ import { AppBar, Button, Flex, Stack, Text, Tooltip } from '@nvidia/foundations-
import { Breadcrumbs } from '@studio/components/Breadcrumbs';
import { UserPopover } from '@studio/components/UserPopover';
import { TOUR_ENABLED } from '@studio/constants/environment';
+import { ROUTES } from '@studio/constants/routes';
import { useWorkspaceFromPathIfExists } from '@studio/hooks/useWorkspaceFromPath';
+import { ClaudeCodeTopBarChat } from '@studio/routes/agents/ClaudeCodeChatRoute/ClaudeCodeTopBarChat';
import { ThemeSwitch } from '@studio/routes/PageLayout/ThemeSwitch';
import { getWorkspaceDetailsDefaultRoute } from '@studio/routes/utils';
import { useSidebarState } from '@studio/util/hooks/useSidebarState';
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
import { lazy, Suspense, type FC, type ReactNode } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, matchPath, useLocation } from 'react-router-dom';
const WelcomeTour = lazy(() =>
import('@studio/components/WelcomeTour').then((m) => ({ default: m.WelcomeTour }))
@@ -24,6 +26,12 @@ interface Props {
export const GlobalNav: FC = ({ sideNav }) => {
const { expanded, toggle } = useSidebarState();
const workspace = useWorkspaceFromPathIfExists();
+ const location = useLocation();
+ const isDashboardRoute =
+ matchPath({ path: ROUTES.workspace.dashboard, end: true }, location.pathname) !== null;
+ const isClaudeCodeChatRoute =
+ matchPath({ path: ROUTES.workspace.claudeCodeChat, end: true }, location.pathname) !== null;
+ const shouldMountClaudeCodeTopBarChat = !isDashboardRoute && !isClaudeCodeChatRoute;
const toggleLabel = expanded ? 'Collapse sidebar' : 'Expand sidebar';
const ToggleSidebarButton = (
@@ -71,6 +79,7 @@ export const GlobalNav: FC = ({ sideNav }) => {
)}
+ {shouldMountClaudeCodeTopBarChat && }
diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx
index 7da013ee10..01900ba2f5 100644
--- a/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx
+++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.test.tsx
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { ROUTES } from '@studio/constants/routes';
+import { getClaudeCodeActiveSessionStorageKey } from '@studio/routes/agents/ClaudeCodeChatRoute/activeSessionStorage';
import { DashboardLandingRoute } from '@studio/routes/DashboardLandingRoute';
import { mockFeatureFlags } from '@studio/tests/util/mockFeatureFlags';
import { TestProviders } from '@studio/tests/util/TestProviders';
@@ -59,6 +60,7 @@ const renderRoute = () => {
describe('DashboardLandingRoute', () => {
beforeEach(() => {
+ localStorage.clear();
vi.clearAllMocks();
mockFeatureFlags({
agentsEnabled: true,
@@ -332,6 +334,17 @@ describe('DashboardLandingRoute', () => {
);
});
+ it('clears the active Claude Code session before starting from the landing composer', async () => {
+ const user = userEvent.setup();
+ localStorage.setItem(getClaudeCodeActiveSessionStorageKey(workspace), 'session-existing');
+ renderRoute();
+
+ await user.type(await screen.findByRole('textbox', { name: 'Message Claude' }), 'Check repo');
+ await user.click(screen.getByRole('button', { name: 'Send message' }));
+
+ expect(localStorage.getItem(getClaudeCodeActiveSessionStorageKey(workspace))).toBeNull();
+ });
+
it('submits the landing composer when Enter is pressed', async () => {
const user = userEvent.setup();
renderRoute();
diff --git a/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx
index a8ee3e3879..9e6233429e 100644
--- a/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx
+++ b/web/packages/studio/src/routes/DashboardLandingRoute/index.tsx
@@ -6,6 +6,7 @@ import { Button, Flex, Text, TextArea, Tooltip } from '@nvidia/foundations-react
import { AccessibleTitle } from '@studio/components/AccessibleTitle';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs';
+import { writeStoredActiveSessionId } from '@studio/routes/agents/ClaudeCodeChatRoute/activeSessionStorage';
import {
CLAUDE_CODE_SKILLS_QUERY_KEY,
listClaudeCodeSkills,
@@ -153,6 +154,7 @@ export const DashboardLandingRoute: FC = () => {
const handleSubmit = useCallback(
(prompt: string) => {
+ writeStoredActiveSessionId(workspace, null);
const state: ClaudeCodeChatRouteState = { initialPrompt: prompt };
navigate(getClaudeCodeChatRoute(workspace), { state });
},
diff --git a/web/packages/studio/src/routes/PageLayout/index.tsx b/web/packages/studio/src/routes/PageLayout/index.tsx
index 5858a4106b..e74fd5bb0e 100644
--- a/web/packages/studio/src/routes/PageLayout/index.tsx
+++ b/web/packages/studio/src/routes/PageLayout/index.tsx
@@ -2,9 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
import { GlobalNav } from '@studio/components/Layouts/GlobalNav';
+import { CODING_AGENT_STUDIO_ENABLED } from '@studio/constants/environment';
+import { useWorkspaceFromPathIfExists } from '@studio/hooks/useWorkspaceFromPath';
import { useAuthAutoLogin } from '@studio/providers/auth';
import { useAuthTokenStatus } from '@studio/providers/auth/useAuthTokenStatus';
import { useSelectedWorkspace } from '@studio/providers/workspace';
+import { ClaudeCodeChatProvider } from '@studio/routes/agents/ClaudeCodeChatRoute/context/ClaudeCodeChatProvider';
import { WorkspaceGuard } from '@studio/routes/RootLayout/WorkspaceGuard';
import { ReactNode } from 'react';
import { Outlet } from 'react-router-dom';
@@ -13,6 +16,7 @@ export const PageLayout = ({ sideNav }: { sideNav?: (collapsed: boolean) => Reac
const { isAuthPending } = useAuthAutoLogin();
const { isTokenActive } = useAuthTokenStatus();
const { selectedWorkspace, isWorkspaceUnauthorized, isWorkspaceLoading } = useSelectedWorkspace();
+ const workspace = useWorkspaceFromPathIfExists();
if (isAuthPending) {
return null;
@@ -25,16 +29,31 @@ export const PageLayout = ({ sideNav }: { sideNav?: (collapsed: boolean) => Reac
? "[grid-template-areas:'logobar_navbar''content_content']"
: "[grid-template-areas:'logobar_navbar''sidebar_content']";
- return (
-
+ // Rendered as direct grid children; the provider adds no DOM of its own, so
+ // it can wrap both the nav (pop-out chat) and the outlet (full chat) while
+ // keeping a single shared chat runtime alive across workspace navigation.
+ const layout = (
+ <>