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
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,38 @@ By default, Agent UI connects to `http://localhost:7777`. You can easily change

> **Warning**: Make sure your AgentOS is actually running on the specified endpoint before attempting to connect.

### 3. Test the Connection
### 3. Configure Authentication (Optional)

If your AgentOS instance requires authentication, you can configure it in two ways:

#### Option 1: Environment Variable (Recommended)

Set the `OS_SECURITY_KEY` environment variable:

```bash
# In your .env.local file or shell environment
NEXT_PUBLIC_OS_SECURITY_KEY=your_auth_token_here
```

> **Note**: This uses the same environment variable as AgentOS, so if you're running both on the same machine, you only need to set it once. The token will be automatically loaded when the application starts.

#### Option 2: UI Configuration

1. In the left sidebar, locate the "Auth Token" section
2. Click on the token field to edit it
3. Enter your authentication token
4. The token will be securely stored and included in all API requests

> **Security Note**: Authentication tokens are stored locally in global store and are included as Bearer tokens in API requests to your AgentOS instance.

### 4. Test the Connection

Once you've configured the endpoint:

1. The Agent UI will automatically attempt to connect to your AgentOS
2. If successful, you'll see your agents available in the chat interface
3. If there are connection issues, check that your AgentOS is running and accessible. Check out the troubleshooting guide [here](https://docs.agno.com/faq/agentos-connection)



## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines.
Expand Down
63 changes: 49 additions & 14 deletions src/api/os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,29 @@ import { APIRoutes } from './routes'

import { AgentDetails, Sessions, TeamDetails } from '@/types/os'

// Helper function to create headers with optional auth token
const createHeaders = (authToken?: string): HeadersInit => {
const headers: HeadersInit = {
'Content-Type': 'application/json'
}

if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`
}

return headers
}

export const getAgentsAPI = async (
endpoint: string
endpoint: string,
authToken?: string
): Promise<AgentDetails[]> => {
const url = APIRoutes.GetAgents(endpoint)
try {
const response = await fetch(url, { method: 'GET' })
const response = await fetch(url, {
method: 'GET',
headers: createHeaders(authToken)
})
if (!response.ok) {
toast.error(`Failed to fetch agents: ${response.statusText}`)
return []
Expand All @@ -22,9 +39,13 @@ export const getAgentsAPI = async (
}
}

export const getStatusAPI = async (base: string): Promise<number> => {
export const getStatusAPI = async (
base: string,
authToken?: string
): Promise<number> => {
const response = await fetch(APIRoutes.Status(base), {
method: 'GET'
method: 'GET',
headers: createHeaders(authToken)
})
return response.status
}
Expand All @@ -33,7 +54,8 @@ export const getAllSessionsAPI = async (
base: string,
type: 'agent' | 'team',
componentId: string,
dbId: string
dbId: string,
authToken?: string
): Promise<Sessions | { data: [] }> => {
try {
const url = new URL(APIRoutes.GetSessions(base))
Expand All @@ -42,7 +64,8 @@ export const getAllSessionsAPI = async (
url.searchParams.set('db_id', dbId)

const response = await fetch(url.toString(), {
method: 'GET'
method: 'GET',
headers: createHeaders(authToken)
})

if (!response.ok) {
Expand All @@ -61,7 +84,8 @@ export const getSessionAPI = async (
base: string,
type: 'agent' | 'team',
sessionId: string,
dbId?: string
dbId?: string,
authToken?: string
) => {
// build query string
const queryParams = new URLSearchParams({ type })
Expand All @@ -70,7 +94,8 @@ export const getSessionAPI = async (
const response = await fetch(
`${APIRoutes.GetSession(base, sessionId)}?${queryParams.toString()}`,
{
method: 'GET'
method: 'GET',
headers: createHeaders(authToken)
}
)

Expand All @@ -84,23 +109,31 @@ export const getSessionAPI = async (
export const deleteSessionAPI = async (
base: string,
dbId: string,
sessionId: string
sessionId: string,
authToken?: string
) => {
const queryParams = new URLSearchParams()
if (dbId) queryParams.append('db_id', dbId)
const response = await fetch(
`${APIRoutes.DeleteSession(base, sessionId)}?${queryParams.toString()}`,
{
method: 'DELETE'
method: 'DELETE',
headers: createHeaders(authToken)
}
)
return response
}

export const getTeamsAPI = async (endpoint: string): Promise<TeamDetails[]> => {
export const getTeamsAPI = async (
endpoint: string,
authToken?: string
): Promise<TeamDetails[]> => {
const url = APIRoutes.GetTeams(endpoint)
try {
const response = await fetch(url, { method: 'GET' })
const response = await fetch(url, {
method: 'GET',
headers: createHeaders(authToken)
})
if (!response.ok) {
toast.error(`Failed to fetch teams: ${response.statusText}`)
return []
Expand All @@ -117,12 +150,14 @@ export const getTeamsAPI = async (endpoint: string): Promise<TeamDetails[]> => {
export const deleteTeamSessionAPI = async (
base: string,
teamId: string,
sessionId: string
sessionId: string,
authToken?: string
) => {
const response = await fetch(
APIRoutes.DeleteTeamSession(base, teamId, sessionId),
{
method: 'DELETE'
method: 'DELETE',
headers: createHeaders(authToken)
}
)

Expand Down
5 changes: 4 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { ChatArea } from '@/components/chat/ChatArea'
import { Suspense } from 'react'

export default function Home() {
// Check if OS_SECURITY_KEY is defined on server-side
const hasEnvToken = !!process.env.NEXT_PUBLIC_OS_SECURITY_KEY
const envToken = process.env.NEXT_PUBLIC_OS_SECURITY_KEY || ''
return (
<Suspense fallback={<div>Loading...</div>}>
<div className="flex h-screen bg-background/80">
<Sidebar />
<Sidebar hasEnvToken={hasEnvToken} envToken={envToken} />
<ChatArea />
</div>
</Suspense>
Expand Down
143 changes: 143 additions & 0 deletions src/components/chat/Sidebar/AuthToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use client'
import { Button } from '@/components/ui/button'
import { useStore } from '@/store'
import { motion, AnimatePresence } from 'framer-motion'
import { useState, useEffect } from 'react'
import Icon from '@/components/ui/icon'

const AuthToken = ({
hasEnvToken,
envToken
}: {
hasEnvToken?: boolean
envToken?: string
}) => {
const { authToken, setAuthToken } = useStore()
const [isEditing, setIsEditing] = useState(false)
const [tokenValue, setTokenValue] = useState('')
const [isMounted, setIsMounted] = useState(false)
const [isHovering, setIsHovering] = useState(false)

useEffect(() => {
// Initialize with environment variable if available and no token is set
if (hasEnvToken && envToken && !authToken) {
setAuthToken(envToken)
setTokenValue(envToken)
} else {
setTokenValue(authToken)
}
setIsMounted(true)
}, [authToken, setAuthToken, hasEnvToken, envToken])

const handleSave = () => {
const cleanToken = tokenValue.trim()
setAuthToken(cleanToken)
setIsEditing(false)
setIsHovering(false)
}

const handleCancel = () => {
setTokenValue(authToken)
setIsEditing(false)
setIsHovering(false)
}

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSave()
} else if (e.key === 'Escape') {
handleCancel()
}
}

const handleClear = () => {
setAuthToken('')
setTokenValue('')
}

const displayValue = authToken
? `${'*'.repeat(Math.min(authToken.length, 20))}${authToken.length > 20 ? '...' : ''}`
: 'NO TOKEN SET'

return (
<div className="flex flex-col items-start gap-2">
<div className="text-xs font-medium uppercase text-primary">
Auth Token
</div>
{isEditing ? (
<div className="flex w-full items-center gap-1">
<input
type="password"
value={tokenValue}
onChange={(e) => setTokenValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter authentication token..."
className="flex h-9 w-full items-center text-ellipsis rounded-xl border border-primary/15 bg-accent p-3 text-xs font-medium text-muted placeholder:text-muted/50"
autoFocus
/>
<Button
variant="ghost"
size="icon"
onClick={handleSave}
className="hover:cursor-pointer hover:bg-transparent"
>
<Icon type="save" size="xs" />
</Button>
</div>
) : (
<div className="flex w-full items-center gap-1">
<motion.div
className="relative flex h-9 w-full cursor-pointer items-center justify-between rounded-xl border border-primary/15 bg-accent p-3 uppercase"
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
onClick={() => setIsEditing(true)}
transition={{ type: 'spring', stiffness: 400, damping: 10 }}
>
<AnimatePresence mode="wait">
{isHovering ? (
<motion.div
key="token-display-hover"
className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<p className="flex items-center gap-2 whitespace-nowrap text-xs font-medium text-primary">
<Icon type="edit" size="xxs" /> EDIT TOKEN
</p>
</motion.div>
) : (
<motion.div
key="token-display"
className="absolute inset-0 flex items-center justify-between px-3"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<p className="text-xs font-medium text-muted">
{isMounted ? displayValue : 'NO TOKEN SET'}
</p>
</motion.div>
)}
</AnimatePresence>
</motion.div>
{authToken && (
<Button
variant="ghost"
size="icon"
onClick={handleClear}
className="hover:cursor-pointer hover:bg-transparent"
title="Clear token"
>
<Icon type="x" size="xs" />
</Button>
)}
</div>
)}
</div>
)
}

export default AuthToken
4 changes: 3 additions & 1 deletion src/components/chat/Sidebar/Sessions/SessionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const SessionItem = ({
const [teamId] = useQueryState('team')
const [dbId] = useQueryState('db_id')
const [, setSessionId] = useQueryState('session')
const authToken = useStore((state) => state.authToken)
const { getSession } = useSessionLoader()
const { selectedEndpoint, sessionsData, setSessionsData, mode } = useStore()
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
Expand Down Expand Up @@ -56,7 +57,8 @@ const SessionItem = ({
const response = await deleteSessionAPI(
selectedEndpoint,
dbId ?? '',
session_id
session_id,
authToken
)

if (response?.ok && sessionsData) {
Expand Down
4 changes: 1 addition & 3 deletions src/components/chat/Sidebar/Sessions/Sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const SkeletonList: FC<SkeletonListProps> = ({ skeletonCount }) => {

const Sessions = () => {
const [agentId] = useQueryState('agent', {
parse: (v) => v || undefined,
parse: (v: string | null) => v || undefined,
history: 'push'
})
const [teamId] = useQueryState('team')
Expand All @@ -51,8 +51,6 @@ const Sessions = () => {
isSessionsLoading
} = useStore()

console.log({ sessionsData })

const [isScrolling, setIsScrolling] = useState(false)
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(
null
Expand Down
Loading