chore(dev): show custom agent name on thread page

This commit is contained in:
Soumyadas15 2025-05-24 13:03:17 +05:30
parent beeaffb533
commit cb36e534b2
8 changed files with 357 additions and 214 deletions

View File

@ -72,6 +72,11 @@ class AgentResponse(BaseModel):
created_at: str
updated_at: str
class ThreadAgentResponse(BaseModel):
agent: Optional[AgentResponse]
source: str # "thread", "default", "none", "missing"
message: str
def initialize(
_thread_manager: ThreadManager,
_db: DBConnection,
@ -401,6 +406,78 @@ async def get_agent_run(agent_run_id: str, user_id: str = Depends(get_current_us
"error": agent_run_data['error']
}
@router.get("/thread/{thread_id}/agent", response_model=ThreadAgentResponse)
async def get_thread_agent(thread_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
"""Get the agent details for a specific thread."""
logger.info(f"Fetching agent details for thread: {thread_id}")
client = await db.client
try:
# Verify thread access and get thread data including agent_id
await verify_thread_access(client, thread_id, user_id)
thread_result = await client.table('threads').select('agent_id', 'account_id').eq('thread_id', thread_id).execute()
if not thread_result.data:
raise HTTPException(status_code=404, detail="Thread not found")
thread_data = thread_result.data[0]
thread_agent_id = thread_data.get('agent_id')
account_id = thread_data.get('account_id')
# If no agent_id is set in the thread, try to get the default agent
effective_agent_id = thread_agent_id
agent_source = "thread"
if not effective_agent_id:
# No agent set in thread, get default agent for the account
default_agent_result = await client.table('agents').select('agent_id').eq('account_id', account_id).eq('is_default', True).execute()
if default_agent_result.data:
effective_agent_id = default_agent_result.data[0]['agent_id']
agent_source = "default"
else:
# No default agent found
return {
"agent": None,
"source": "none",
"message": "No agent configured for this thread"
}
# Fetch the agent details
agent_result = await client.table('agents').select('*').eq('agent_id', effective_agent_id).eq('account_id', account_id).execute()
if not agent_result.data:
# Agent was deleted or doesn't exist
return {
"agent": None,
"source": "missing",
"message": f"Agent {effective_agent_id} not found or was deleted"
}
agent_data = agent_result.data[0]
return {
"agent": AgentResponse(
agent_id=agent_data['agent_id'],
account_id=agent_data['account_id'],
name=agent_data['name'],
description=agent_data.get('description'),
system_prompt=agent_data['system_prompt'],
configured_mcps=agent_data.get('configured_mcps', []),
agentpress_tools=agent_data.get('agentpress_tools', {}),
is_default=agent_data.get('is_default', False),
created_at=agent_data['created_at'],
updated_at=agent_data['updated_at']
),
"source": agent_source,
"message": f"Using {agent_source} agent: {agent_data['name']}"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching agent for thread {thread_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to fetch thread agent: {str(e)}")
@router.get("/agent-run/{agent_run_id}/stream")
async def stream_agent_run(
agent_run_id: str,

View File

@ -23,7 +23,6 @@ import {
} from '@/lib/api';
import { toast } from 'sonner';
import { ChatInput } from '@/components/thread/chat-input/chat-input';
import { AgentSelector } from '@/components/thread/chat-input/agent-selector';
import { FileViewerModal } from '@/components/thread/file-viewer-modal';
import { SiteHeader } from '@/components/thread/thread-site-header';
import {
@ -47,6 +46,7 @@ import {
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import {
UnifiedMessage,
@ -63,6 +63,7 @@ import { useAgentRunsQuery, useStartAgentMutation, useStopAgentMutation } from '
import { useBillingStatusQuery } from '@/hooks/react-query/threads/use-billing-status';
import { useSubscription, isPlan } from '@/hooks/react-query/subscriptions/use-subscriptions';
import { SubscriptionStatus } from '@/components/thread/chat-input/_use-model-selection';
import { useThreadAgent } from '@/hooks/react-query/agents/use-agents';
// Extend the base Message type with the expected database fields
interface ApiMessageType extends BaseApiMessageType {
@ -136,6 +137,7 @@ export default function ThreadPage({
const messagesLoadedRef = useRef(false);
const agentRunsCheckedRef = useRef(false);
const previousAgentStatus = useRef<typeof agentStatus>('idle');
const { data: threadAgent, isLoading: threadAgentLoading, error: threadAgentError } = useThreadAgent(threadId);
const [externalNavIndex, setExternalNavIndex] = React.useState<number | undefined>(undefined);
@ -1223,11 +1225,17 @@ export default function ThreadPage({
"mx-auto",
isMobile ? "w-full px-4" : "max-w-3xl"
)}>
{threadAgentLoading || threadAgentError ? (
<div className="space-y-3">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-12 w-full rounded-lg" />
</div>
) : (
<ChatInput
value={newMessage}
onChange={setNewMessage}
onSubmit={handleSubmitMessage}
placeholder="Ask Suna anything..."
placeholder={`Ask ${threadAgent?.agent?.name || 'Suna'} anything...`}
loading={isSending}
disabled={isSending || agentStatus === 'running' || agentStatus === 'connecting'}
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
@ -1235,7 +1243,9 @@ export default function ThreadPage({
autoFocus={!isLoading}
onFileBrowse={handleOpenFileViewer}
sandboxId={sandboxId || undefined}
agentName={threadAgent?.agent?.name || 'Suna'}
/>
)}
</div>
</div>
</div>
@ -1257,6 +1267,7 @@ export default function ThreadPage({
renderAssistantMessage={toolViewAssistant}
renderToolResult={toolViewResult}
isLoading={!initialLoadCompleted.current || isLoading}
agentName={threadAgentLoading ? 'Loading...' : (threadAgent?.agent?.name || 'Suna')}
/>
{sandboxId && (

View File

@ -129,7 +129,7 @@ export function AgentSelector({
)}
</div>
<span className="text-xs text-muted-foreground pl-6 line-clamp-2">
Your personal AI assistant
Your personal AI employee
</span>
</DropdownMenuItem>
{agents.length > 0 ? (
@ -227,7 +227,7 @@ export function AgentSelector({
</span>
) : isUsingSuna ? (
<span className="text-xs text-muted-foreground line-clamp-1 max-w-[150px]">
Your personal AI assistant
Your personal AI employee
</span>
) : null}
</div>
@ -254,7 +254,7 @@ export function AgentSelector({
)}
</div>
<span className="text-xs text-muted-foreground pl-6 line-clamp-2">
Your personal AI assistant
Your personal AI employee
</span>
</DropdownMenuItem>
{agents.length > 0 ? (

View File

@ -39,6 +39,7 @@ export interface ChatInputProps {
hideAttachments?: boolean;
selectedAgentId?: string;
onAgentSelect?: (agentId: string | undefined) => void;
agentName?: string;
}
export interface UploadedFile {
@ -66,6 +67,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
hideAttachments = false,
selectedAgentId,
onAgentSelect,
agentName,
},
ref,
) => {
@ -263,7 +265,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
>
<div className="text-xs text-muted-foreground flex items-center gap-2">
<Loader2 className="h-3 w-3 animate-spin" />
<span>Kortix Suna is working...</span>
<span>{agentName ? `${agentName} is working...` : 'Suna is working...'}</span>
</div>
</motion.div>
)}

View File

@ -45,6 +45,7 @@ interface ToolCallSidePanelProps {
isSuccess?: boolean,
) => React.ReactNode;
isLoading?: boolean;
agentName?: string;
}
interface ToolCallSnapshot {
@ -65,6 +66,7 @@ export function ToolCallSidePanel({
project,
isLoading = false,
externalNavigateToIndex,
agentName
}: ToolCallSidePanelProps) {
const [dots, setDots] = React.useState('');
const [internalIndex, setInternalIndex] = React.useState(0);
@ -233,7 +235,7 @@ export function ToolCallSidePanel({
<div className="ml-2 flex items-center gap-2">
<Computer className="h-4 w-4" />
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
Computer View
{agentName ? `${agentName}'s Computer` : 'Suna\'s Computer'}
</h2>
</div>
<Button
@ -269,7 +271,7 @@ export function ToolCallSidePanel({
<div className="ml-2 flex items-center gap-2">
<Computer className="h-4 w-4" />
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
Computer View
{agentName ? `${agentName}'s Computer` : 'Suna\'s Computer'}
</h2>
</div>
<Button
@ -330,7 +332,7 @@ export function ToolCallSidePanel({
<div className="ml-2 flex items-center gap-2">
<Computer className="h-4 w-4" />
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
Computer View
{agentName ? `${agentName}'s Computer` : 'Suna\'s Computer'}
</h2>
</div>

View File

@ -6,4 +6,6 @@ export const agentKeys = createQueryKeys({
list: (filters?: Record<string, any>) => [...agentKeys.lists(), filters] as const,
details: () => [...agentKeys.all, 'detail'] as const,
detail: (id: string) => [...agentKeys.details(), id] as const,
});
threadAgents: () => [...agentKeys.all, 'thread-agent'] as const,
threadAgent: (threadId: string) => [...agentKeys.threadAgents(), threadId] as const,
});

View File

@ -2,7 +2,7 @@ import { createMutationHook, createQueryHook } from '@/hooks/use-query';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { agentKeys } from './keys';
import { Agent, AgentUpdateRequest, createAgent, deleteAgent, getAgent, getAgents, updateAgent } from './utils';
import { Agent, AgentUpdateRequest, createAgent, deleteAgent, getAgent, getAgents, getThreadAgent, updateAgent } from './utils';
export const useAgents = createQueryHook(
agentKeys.list(),
@ -92,3 +92,15 @@ export const useOptimisticAgentUpdate = () => {
},
};
};
export const useThreadAgent = (threadId: string) => {
return createQueryHook(
agentKeys.threadAgent(threadId),
() => getThreadAgent(threadId),
{
enabled: !!threadId,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
}
)();
};

View File

@ -16,9 +16,15 @@ export type Agent = {
is_default: boolean;
created_at: string;
updated_at: string;
};
};
export type AgentCreateRequest = {
export type ThreadAgentResponse = {
agent: Agent | null;
source: 'thread' | 'default' | 'none' | 'missing';
message: string;
};
export type AgentCreateRequest = {
name: string;
description?: string;
system_prompt: string;
@ -28,9 +34,9 @@ export type Agent = {
}>;
agentpress_tools?: Record<string, any>;
is_default?: boolean;
};
};
export type AgentUpdateRequest = {
export type AgentUpdateRequest = {
name?: string;
description?: string;
system_prompt?: string;
@ -40,9 +46,9 @@ export type Agent = {
}>;
agentpress_tools?: Record<string, any>;
is_default?: boolean;
};
};
export const getAgents = async (): Promise<Agent[]> => {
export const getAgents = async (): Promise<Agent[]> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -71,9 +77,9 @@ export type Agent = {
console.error('Error fetching agents:', err);
throw err;
}
};
};
export const getAgent = async (agentId: string): Promise<Agent> => {
export const getAgent = async (agentId: string): Promise<Agent> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -102,9 +108,9 @@ export type Agent = {
console.error('Error fetching agent:', err);
throw err;
}
};
};
export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent> => {
export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -134,9 +140,9 @@ export type Agent = {
console.error('Error creating agent:', err);
throw err;
}
};
};
export const updateAgent = async (agentId: string, agentData: AgentUpdateRequest): Promise<Agent> => {
export const updateAgent = async (agentId: string, agentData: AgentUpdateRequest): Promise<Agent> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -166,9 +172,9 @@ export type Agent = {
console.error('Error updating agent:', err);
throw err;
}
};
};
export const deleteAgent = async (agentId: string): Promise<void> => {
export const deleteAgent = async (agentId: string): Promise<void> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -195,5 +201,36 @@ export type Agent = {
console.error('Error deleting agent:', err);
throw err;
}
};
};
export const getThreadAgent = async (threadId: string): Promise<ThreadAgentResponse> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('You must be logged in to get thread agent details');
}
const response = await fetch(`${API_URL}/thread/${threadId}/agent`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
}
const threadAgent = await response.json();
console.log('[API] Fetched thread agent:', threadAgent.source, threadAgent.agent?.name);
return threadAgent;
} catch (err) {
console.error('Error fetching thread agent:', err);
throw err;
}
};