Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ interface DeploymentLogsViewProps {
workspace: string;
/** All known deployments for the active agent — any status. */
deployments: AgentDeployment[];
/** Controlled selection — when provided, the picker reflects this deployment. */
selectedDeploymentName?: string;
/** Called when the user (or a caller) changes the selected deployment. */
onSelectDeployment?: (name: string) => void;
}

export const DeploymentLogsView: FC<DeploymentLogsViewProps> = ({ workspace, deployments }) => {
export const DeploymentLogsView: FC<DeploymentLogsViewProps> = ({
workspace,
deployments,
selectedDeploymentName,
onSelectDeployment,
}) => {
const sortedDeployments = useMemo(
() =>
[...deployments].sort((a, b) => {
Expand All @@ -31,18 +40,25 @@ export const DeploymentLogsView: FC<DeploymentLogsViewProps> = ({ workspace, dep
}),
[deployments]
);
const [selectedName, setSelectedName] = useState<string | undefined>(
() => sortedDeployments[0]?.name
const [internalName, setInternalName] = useState<string | undefined>(
() => selectedDeploymentName ?? sortedDeployments[0]?.name
);
const isControlled = selectedDeploymentName !== undefined;
const selectedName = isControlled ? selectedDeploymentName : internalName;
const setSelectedName = (name: string) => {
if (!isControlled) setInternalName(name);
onSelectDeployment?.(name);
};

useEffect(() => {
if (!selectedName) {
setSelectedName(sortedDeployments[0]?.name);
if (isControlled) return;
if (!internalName) {
setInternalName(sortedDeployments[0]?.name);
return;
}
const stillPresent = sortedDeployments.some((d) => d.name === selectedName);
if (!stillPresent) setSelectedName(sortedDeployments[0]?.name);
}, [sortedDeployments, selectedName]);
const stillPresent = sortedDeployments.some((d) => d.name === internalName);
if (!stillPresent) setInternalName(sortedDeployments[0]?.name);
}, [sortedDeployments, internalName, isControlled]);

if (sortedDeployments.length === 0) {
return (
Expand All @@ -53,11 +69,12 @@ export const DeploymentLogsView: FC<DeploymentLogsViewProps> = ({ workspace, dep
</Stack>
);
}
const pOffset = '2';

return (
<Stack className="h-full min-h-0" gap="0">
<Stack className="h-full min-h-0 w-full" gap="2">
{sortedDeployments.length > 1 && (
<Block padding="4" className="border-b border-base shrink-0">
<Block className="shrink-0" paddingX={pOffset}>
<Select
value={selectedName ?? ''}
items={sortedDeployments.flatMap((d) =>
Expand All @@ -74,7 +91,7 @@ export const DeploymentLogsView: FC<DeploymentLogsViewProps> = ({ workspace, dep
/>
</Block>
)}
<Block className="flex-1 min-h-0 overflow-auto" padding="4">
<Block className="flex-1 min-h-0" paddingX={pOffset}>
{selectedName ? (
<LogsForDeployment workspace={workspace} deploymentName={selectedName} />
) : null}
Expand All @@ -89,32 +106,25 @@ interface LogsForDeploymentProps {
}

const TAIL_LINES = 500;
// Cap the live buffer so a long-lived, noisy stream can't grow state unbounded.
const MAX_STREAMED_LINES = 5000;

const LogsForDeployment: FC<LogsForDeploymentProps> = ({ workspace, deploymentName }) => {
const { data, isLoading } = useAgentsGetDeploymentLogs(
workspace,
deploymentName,
{ tail: TAIL_LINES },
// Short staleTime: the live SSE stream keeps logs current after mount, so we
// only need a fresh tail baseline, not a refetch on every quick tab toggle.
{ query: { staleTime: 5000 } }
);

const [streamedLines, setStreamedLines] = useState<PlatformJobLog[]>([]);
// Reset on deployment change so we don't stitch one process's tail onto another.
useEffect(() => {
setStreamedLines([]);
}, [deploymentName]);

const accessToken = useAuth()?.user?.access_token;
// Byte offset just past the tail; resume the stream from here so lines written
// between the tail fetch and the stream opening aren't dropped.
const tailOffset = data?.next_offset;

useEffect(() => {
// Wait for the tail query to settle so the stream can resume from tailOffset.
if (!deploymentName || isLoading) return;
const url = `${PLATFORM_BASE_URL}${getAgentsStreamDeploymentLogsQueryKey(workspace, deploymentName)[0]}`;
const controller = new AbortController();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import SafeSynthesizerLogo from '@nemo/common/src/svgs/safe_synthesizer_logo.svg
import { NavigationDrawer } from '@studio/components/Layouts/NavigationDrawer';
import { DataDesignerIconFc } from '@studio/constants/constants';
import {
AGENTS_ENABLED,
BASE_MODELS_ENABLED,
CODING_AGENT_STUDIO_ENABLED,
CUSTOMIZER_ENABLED,
Expand All @@ -24,11 +23,8 @@ import {
} from '@studio/constants/environment';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { iconColorClass } from '@studio/routes/constants';
import { getAgentSideNavItems } from '@studio/routes/groups/agentRoutes';
import {
getAgentEvaluationsListRoute,
getAgentsListRoute,
getAgentMonitorRoute,
getAgentOptimizationsRoute,
getDataDesignerJobListRoute,
getModelCompareRoute,
getEvaluationResultsRoute,
Expand All @@ -50,7 +46,6 @@ import {
Boxes,
ChartBar,
Database,
HatGlasses,
ListChecks,
Home,
ShieldCheck,
Expand All @@ -59,9 +54,6 @@ import {
Cog,
Columns3,
Rocket,
Lightbulb,
Activity,
FlaskConical,
Waypoints,
} from 'lucide-react';
import { useMemo } from 'react';
Expand Down Expand Up @@ -157,35 +149,7 @@ export const WorkspaceSideNav = ({ collapsed }: { collapsed?: boolean }) => {
]
: [];

const agentsNav = AGENTS_ENABLED
? [
{
id: 'agents',
slotIcon: <HatGlasses className={iconColorClass} />,
slotLabel: 'Agents',
href: getAgentsListRoute(workspace),
},
{
id: 'agent-evaluations',
slotIcon: <FlaskConical className={iconColorClass} />,
slotLabel: 'Evaluations',
href: getAgentEvaluationsListRoute(workspace),
},
{
id: 'agent-monitor',
slotIcon: <Activity className={iconColorClass} />,
slotLabel: 'Monitor',
href: getAgentMonitorRoute(workspace),
},

{
id: 'agent-optimizations',
slotIcon: <Lightbulb className={iconColorClass} />,
slotLabel: 'Suggestions',
href: getAgentOptimizationsRoute(workspace),
},
]
: [];
const agentItems = getAgentSideNavItems(workspace);

const virtualModelsNav = [
{
Expand Down Expand Up @@ -237,11 +201,11 @@ export const WorkspaceSideNav = ({ collapsed }: { collapsed?: boolean }) => {
return [
...dashboardNav,
...modelCompareNav,
...(agentsNav.length > 0
...(agentItems.length > 0
? [
{
group: 'Agents',
items: agentsNav,
items: agentItems,
},
]
: []),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { KVPair } from '@nemo/common/src/components/KVPair';
import { isDefined } from '@nemo/common/src/utils/list';
import type { Agent } from '@nemo/sdk/generated/agents/schema/Agent';
import { Stack } from '@nvidia/foundations-react-core';
import type { AgentConfig } from '@studio/components/dataViews/AgentsDataView';
import { getAgentModelNames } from '@studio/components/dataViews/AgentsDataView/utils';
import { DetailPanel } from '@studio/routes/agents/AgentDetailRoute/overview/DetailPanel';
import type { FC } from 'react';

interface ConfigurationTabProps {
workspace: string;
agentName?: string;
agent?: Agent;
}

/** Read-only view of the agent's configuration metadata. */
export const ConfigurationTab: FC<ConfigurationTabProps> = ({ workspace, agentName, agent }) => {
const models = getAgentModelNames(agent?.config as AgentConfig | undefined);

return (
<DetailPanel title="Configuration">
<Stack gap="2">
<KVPair label="Name" value={agent?.name ?? agentName} />
<KVPair label="Workspace" value={agent?.workspace ?? workspace} />
{isDefined(agent?.description) && (
<KVPair label="Description" value={agent.description || '-'} />
)}
{models.length > 0 && <KVPair label="Model" value={models.join(', ')} />}
{isDefined(agent?.config_format) && (
<KVPair label="Config Format" value={agent.config_format} />
)}
</Stack>
</DetailPanel>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { StatusBadge } from '@nemo/common/src/components/StatusBadge';
import type { AgentDeployment } from '@nemo/sdk/generated/agents/schema/AgentDeployment';
import { Button, Flex, Stack, StatusIndicator, Text } from '@nvidia/foundations-react-core';
import { deploymentStatusColor } from '@studio/components/sidePanels/AgentPanels/AgentPanel/helpers';
import { NoHealthyDeploymentsBanner } from '@studio/components/sidePanels/AgentPanels/AgentPanel/NoHealthyDeploymentsBanner';
import { DetailPanel } from '@studio/routes/agents/AgentDetailRoute/overview/DetailPanel';
import type { FC } from 'react';

interface DeploymentsTabProps {
agentName?: string;
deployments: AgentDeployment[];
isDeploymentsLoading: boolean;
isDeploying: boolean;
onDeploy: () => void;
onChat: (deployment: AgentDeployment) => void;
onDelete: (deployment: AgentDeployment) => void;
onViewLogs: (deployment: AgentDeployment) => void;
}

/** Deployments list with per-deployment actions. */
export const DeploymentsTab: FC<DeploymentsTabProps> = ({
agentName,
deployments,
isDeploymentsLoading,
isDeploying,
onDeploy,
onChat,
onDelete,
onViewLogs,
}) => (
<Stack gap="5" className="w-full">
<DetailPanel title="Deployments" flush>
{!isDeploymentsLoading && deployments.length === 0 ? (
<div className="p-4">
<NoHealthyDeploymentsBanner
agentName={agentName}
isDeploying={isDeploying}
onDeploy={onDeploy}
message="No deployments for this agent."
/>
</div>
) : (
<Stack gap="0">
{deployments.map((deployment, index) => (
<Flex
key={deployment.name}
align="center"
gap="2"
className={`px-4 py-3 ${index > 0 ? 'border-t border-base' : ''}`}
>
<StatusIndicator color={deploymentStatusColor(deployment.status)} size="small" />
<Stack gap="0" className="min-w-0 flex-1">
<Text kind="body/semibold/sm">{deployment.name}</Text>
{deployment.endpoint && (
<Text kind="body/regular/xs" color="secondary" className="truncate">
{deployment.endpoint}
</Text>
)}
{deployment.error && (
<Text kind="body/regular/xs" color="danger" className="truncate">
{deployment.error}
</Text>
)}
</Stack>
<StatusBadge status={deployment.status} />
<Flex gap="1" className="shrink-0">
<Button
kind="tertiary"
size="small"
disabled={deployment.status !== 'running'}
onClick={() => onChat(deployment)}
>
Chat
</Button>
<Button kind="tertiary" size="small" onClick={() => onViewLogs(deployment)}>
Logs
</Button>
<Button
kind="tertiary"
size="small"
color="danger"
onClick={() => onDelete(deployment)}
>
Delete
</Button>
</Flex>
</Flex>
))}
</Stack>
)}
</DetailPanel>
</Stack>
);
Loading
Loading