From abd3892731cac572da8074f27ba9c4ca1ca75430 Mon Sep 17 00:00:00 2001 From: Danielle Ali <44468613+dmariali@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:58:31 -0400 Subject: [PATCH 1/6] feat(studio): coding agent adjust layout Signed-off-by: Danielle Ali <44468613+dmariali@users.noreply.github.com> --- .../Layouts/GlobalNav/index.test.tsx | 58 ++++++++++++- .../components/Layouts/GlobalNav/index.tsx | 42 +++++++-- .../ClaudeCodeHistoryPanel.test.tsx | 47 ++++++++-- .../ClaudeCodeHistoryPanel.tsx | 85 ++++++++----------- .../ClaudeCodeChatRoute/ClaudeCodeLayout.tsx | 2 +- .../historyPanel/ClaudeCodeArtifactsPane.tsx | 5 +- .../historyPanel/FloatingPanel.tsx | 38 +++++++++ .../historyPanel/constants.tsx | 26 ------ .../historyPanel/helpers.ts | 4 - .../ClaudeCodeChatRoute/historyPanel/types.ts | 2 - .../studio/src/util/hooks/useSidebarState.ts | 7 +- web/packages/studio/src/util/localStorage.ts | 1 - 12 files changed, 217 insertions(+), 100 deletions(-) create mode 100644 web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/historyPanel/FloatingPanel.tsx delete mode 100644 web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/historyPanel/constants.tsx diff --git a/web/packages/studio/src/components/Layouts/GlobalNav/index.test.tsx b/web/packages/studio/src/components/Layouts/GlobalNav/index.test.tsx index 9a6e6e64a1..eab980271d 100644 --- a/web/packages/studio/src/components/Layouts/GlobalNav/index.test.tsx +++ b/web/packages/studio/src/components/Layouts/GlobalNav/index.test.tsx @@ -6,7 +6,7 @@ import { mockUseParams } from '@studio/tests/util/mockUseParams'; import { SIDE_NAV_OPEN_KEY } from '@studio/util/localStorage'; import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { generatePath, MemoryRouter } from 'react-router-dom'; +import { createMemoryRouter, generatePath, MemoryRouter, RouterProvider } from 'react-router-dom'; vi.mock('@studio/components/Breadcrumbs', () => ({ Breadcrumbs: () =>
, @@ -89,6 +89,62 @@ describe('GlobalNav', () => { expect(screen.getByText('NeMo Studio')).toBeInTheDocument(); }); + it('starts collapsed on the Code Agent route when no preference is saved', async () => { + createMatchMediaMock(true); + + await renderGlobalNav( + generatePath(ROUTES.workspace.claudeCodeChat, { workspace: 'test-workspace' }) + ); + + expect(screen.getByRole('button', { name: 'Expand sidebar' })).toBeInTheDocument(); + expect(screen.queryByText('NeMo Studio')).not.toBeInTheDocument(); + }); + + it('respects a saved expanded preference on the Code Agent route', async () => { + localStorage.setItem(SIDE_NAV_OPEN_KEY, JSON.stringify('true')); + createMatchMediaMock(true); + + await renderGlobalNav( + generatePath(ROUTES.workspace.claudeCodeChat, { workspace: 'test-workspace' }) + ); + + expect(screen.getByRole('button', { name: 'Collapse sidebar' })).toBeInTheDocument(); + expect(screen.getByText('NeMo Studio')).toBeInTheDocument(); + }); + + it('uses the Code Agent default during in-app navigation', async () => { + createMatchMediaMock(true); + const { GlobalNav } = await import('@studio/components/Layouts/GlobalNav/index'); + const router = createMemoryRouter( + [ + { + path: '*', + element: ( +
Side Nav Content
} /> + ), + }, + ], + { initialEntries: ['/workspaces/test-workspace/jobs'] } + ); + render(); + + expect(screen.getByRole('button', { name: 'Collapse sidebar' })).toBeInTheDocument(); + + await act(async () => { + await router.navigate( + generatePath(ROUTES.workspace.claudeCodeChat, { workspace: 'test-workspace' }) + ); + }); + + expect(screen.getByRole('button', { name: 'Expand sidebar' })).toBeInTheDocument(); + + await act(async () => { + await router.navigate('/workspaces/test-workspace/jobs'); + }); + + expect(screen.getByRole('button', { name: 'Collapse sidebar' })).toBeInTheDocument(); + }); + it('auto-collapses sidebar when initial viewport is narrow', async () => { createMatchMediaMock(false); await renderGlobalNav(); diff --git a/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx b/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx index 5885cb32c6..0ddd9b6bd7 100644 --- a/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx +++ b/web/packages/studio/src/components/Layouts/GlobalNav/index.tsx @@ -23,15 +23,22 @@ interface Props { sideNav?: (collapsed: boolean) => ReactNode; } -export const GlobalNav: FC = ({ sideNav }) => { - const { expanded, toggle } = useSidebarState(); +interface GlobalNavContentProps extends Props { + isDashboardRoute: boolean; + isClaudeCodeChatRoute: boolean; +} + +const GlobalNavContent: FC = ({ + sideNav, + isDashboardRoute, + isClaudeCodeChatRoute, +}) => { 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 { expanded, toggle } = useSidebarState(!isClaudeCodeChatRoute); const shouldMountClaudeCodeTopBarChat = !isDashboardRoute && !isClaudeCodeChatRoute; + const sidebarBackground = isClaudeCodeChatRoute + ? 'bg-surface-sunken dark:bg-surface-base' + : 'bg-surface-navigation'; const toggleLabel = expanded ? 'Collapse sidebar' : 'Expand sidebar'; const ToggleSidebarButton = ( @@ -51,7 +58,7 @@ export const GlobalNav: FC = ({ sideNav }) => { return ( <> = ({ sideNav }) => { /> {sideNav && (
{sideNav(!expanded)} @@ -98,3 +105,20 @@ export const GlobalNav: FC = ({ sideNav }) => { ); }; + +export const GlobalNav: FC = ({ sideNav }) => { + 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; + + return ( + + ); +}; diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.test.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.test.tsx index 2b6813aff2..995e566ef9 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.test.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.test.tsx @@ -35,7 +35,8 @@ describe('ClaudeCodeHistoryPanel', () => { ]); }); - it('renders history and skills segmented controls', () => { + it('starts history and skills collapsed and expands them independently', async () => { + const user = userEvent.setup(); render( { /> ); - expect(screen.getByRole('radio', { name: 'History' })).toBeChecked(); - expect(screen.getByRole('radio', { name: 'Skills' })).not.toBeChecked(); + const historyButton = screen.getByRole('button', { name: 'Expand Chat history' }); + const skillsButton = screen.getByRole('button', { name: 'Expand Skills' }); + expect(historyButton).toHaveAttribute('aria-expanded', 'false'); + expect(skillsButton).toHaveAttribute('aria-expanded', 'false'); + expect(screen.queryByRole('button', { name: 'New chat' })).not.toBeInTheDocument(); + + await user.click(historyButton); + + expect(screen.getByRole('button', { name: 'Collapse Chat history' })).toHaveAttribute( + 'aria-expanded', + 'true' + ); + expect(skillsButton).toHaveAttribute('aria-expanded', 'false'); expect(screen.getByRole('button', { name: 'New chat' })).toBeInTheDocument(); + + await user.click(skillsButton); + + expect(screen.getByRole('button', { name: 'Expand Chat history' })).toHaveAttribute( + 'aria-expanded', + 'false' + ); + expect(screen.getByRole('button', { name: 'Collapse Skills' })).toHaveAttribute( + 'aria-expanded', + 'true' + ); + expect(screen.queryByRole('button', { name: 'New chat' })).not.toBeInTheDocument(); }); it('renders history sessions and keeps selection working', async () => { @@ -80,6 +104,8 @@ describe('ClaudeCodeHistoryPanel', () => { /> ); + await user.click(screen.getByRole('button', { name: 'Expand Chat history' })); + expect(await screen.findByText('Review the latest agent work')).toBeInTheDocument(); expect(screen.getByText('Bash')).toBeInTheDocument(); @@ -91,6 +117,7 @@ describe('ClaudeCodeHistoryPanel', () => { }); it('shows the summarized title while preserving the full first prompt in the tooltip', async () => { + const user = userEvent.setup(); const firstPrompt = 'I want to create an agent that does spam detection for incoming email.'; mocks.listClaudeCodeHistorySessions.mockResolvedValue([ { @@ -120,6 +147,8 @@ describe('ClaudeCodeHistoryPanel', () => { /> ); + await user.click(screen.getByRole('button', { name: 'Expand Chat history' })); + const sessionButton = await screen.findByRole('button', { name: 'Create Spam Detector Agent now', }); @@ -152,6 +181,14 @@ describe('ClaudeCodeHistoryPanel', () => { ); expect(screen.getByText('Jobs')).toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Chat artifacts' })).toHaveClass( + 'overflow-hidden', + 'rounded', + 'border', + 'shrink-0', + 'bg-surface-base', + 'dark:bg-surface-raised' + ); expect(screen.queryByText('Workspace')).not.toBeInTheDocument(); expect(screen.getByRole('link', { name: /agent-eval-1/ })).toHaveAttribute( 'href', @@ -180,7 +217,7 @@ describe('ClaudeCodeHistoryPanel', () => { expect(screen.getByText('No artifacts yet')).toBeInTheDocument(); }); - it('lists Claude Code skills in the skills tab', async () => { + it('lists Claude Code skills in the expanded skills block', async () => { const user = userEvent.setup(); render( { /> ); - await user.click(screen.getByRole('radio', { name: 'Skills' })); + await user.click(screen.getByRole('button', { name: 'Expand Skills' })); expect(await screen.findByText('Inference')).toBeInTheDocument(); expect(screen.getByText('Use NeMo Platform inference.')).toBeInTheDocument(); diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.tsx index 1f80f719c8..af0d5a18cf 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeHistoryPanel.tsx @@ -1,35 +1,31 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Button, Flex, SegmentedControl, Tooltip } from '@nvidia/foundations-react-core'; +import { Button, Flex, Tooltip } from '@nvidia/foundations-react-core'; import { ClaudeCodeArtifactsPane } from '@studio/routes/agents/ClaudeCodeChatRoute/historyPanel/ClaudeCodeArtifactsPane'; -import { PANEL_TAB_ITEMS } from '@studio/routes/agents/ClaudeCodeChatRoute/historyPanel/constants'; -import { isClaudeCodePanelTab } from '@studio/routes/agents/ClaudeCodeChatRoute/historyPanel/helpers'; +import { FloatingPanel } from '@studio/routes/agents/ClaudeCodeChatRoute/historyPanel/FloatingPanel'; import { HistoryPanelContents } from '@studio/routes/agents/ClaudeCodeChatRoute/historyPanel/HistoryPanelContents'; import { SkillsPanelContents } from '@studio/routes/agents/ClaudeCodeChatRoute/historyPanel/SkillsPanelContents'; import type { ClaudeCodeHistoryPanelProps } from '@studio/routes/agents/ClaudeCodeChatRoute/historyPanel/types'; import { useLocalStorage } from '@studio/util/hooks/useLocalStorage'; -import { CLAUDE_CODE_HISTORY_OPEN_KEY, CLAUDE_CODE_PANEL_TAB_KEY } from '@studio/util/localStorage'; +import { CLAUDE_CODE_HISTORY_OPEN_KEY } from '@studio/util/localStorage'; import { PanelRightClose, PanelRightOpen } from 'lucide-react'; -import { type FC } from 'react'; +import { type FC, useState } from 'react'; + +type OpenFloatingPanel = 'history' | 'skills'; export const ClaudeCodeHistoryPanel: FC = ({ hideArtifacts, ...props }) => { const [historyOpen, setHistoryOpen] = useLocalStorage(CLAUDE_CODE_HISTORY_OPEN_KEY, 'true'); - const [panelTab, setPanelTab] = useLocalStorage(CLAUDE_CODE_PANEL_TAB_KEY, 'history'); + const [openFloatingPanel, setOpenFloatingPanel] = useState(); const isOpen = historyOpen !== 'false'; - const selectedTab = - typeof panelTab === 'string' && isClaudeCodePanelTab(panelTab) ? panelTab : 'history'; const toggleLabel = isOpen ? 'Collapse Claude history' : 'Expand Claude history'; - const handleTabChange = (value: string) => { - if (isClaudeCodePanelTab(value)) setPanelTab(value); - }; if (!isOpen) { return ( -