diff --git a/backend/apps/agent_app.py b/backend/apps/agent_app.py index 89390b2997..7625aa46fb 100644 --- a/backend/apps/agent_app.py +++ b/backend/apps/agent_app.py @@ -837,6 +837,9 @@ async def list_agent_page_api( search_tag_predicates: Optional[str] = Query(None, description="Text-search tag predicates as JSON"), page: int = Query(1, ge=1, description="Page number starting from 1"), page_size: int = Query(20, ge=1, le=100, description="Items per page"), + include_repository_info: bool = Query( + False, description="Include repository listings for agents on this page" + ), authorization: Optional[str] = Header(None), request: Request = None, ): @@ -852,11 +855,13 @@ async def list_agent_page_api( kwargs = { "tenant_id": resolved_tenant_id, "user_id": user_id, + "caller_tenant_id": auth_tenant_id, "permission": permission, "tag": tag, "search": search, "page": page, "page_size": page_size, + "include_repository_info": include_repository_info, } if created_by: kwargs["created_by"] = created_by diff --git a/backend/database/agent_repository_db.py b/backend/database/agent_repository_db.py index 053a11407d..e8da664569 100644 --- a/backend/database/agent_repository_db.py +++ b/backend/database/agent_repository_db.py @@ -354,6 +354,7 @@ def list_agent_repository_by_agent_ids( *, statuses: Collection[str], publisher_tenant_id: str, + publisher_user_id: Optional[str] = None, ) -> List[dict]: """List repository rows for the given agents, scoped to publisher tenant and statuses.""" if not agent_ids: @@ -361,7 +362,7 @@ def list_agent_repository_by_agent_ids( status_list = list(statuses) with get_db_session() as session: - rows = ( + query = ( session.query( AgentRepository.agent_repository_id, AgentRepository.agent_id, @@ -377,7 +378,11 @@ def list_agent_repository_by_agent_ids( AgentRepository.agent_id.in_(agent_ids), AgentRepository.status.in_(status_list), ) - .order_by( + ) + if publisher_user_id is not None: + query = query.filter(AgentRepository.publisher_user_id == publisher_user_id) + rows = ( + query.order_by( AgentRepository.agent_id, AgentRepository.create_time.desc(), ) diff --git a/backend/management/services/agent/management.py b/backend/management/services/agent/management.py index 2e12564abd..8301042879 100644 --- a/backend/management/services/agent/management.py +++ b/backend/management/services/agent/management.py @@ -14,6 +14,7 @@ from agents.create_agent_info import create_tool_config_list from utils.agent_transfer_utils import portable_tool_params, validate_import_tool_params from services.agent_version_service import publish_version_impl +from consts.agent_repository import STATUS_PENDING_REVIEW, STATUS_REJECTED, STATUS_SHARED from consts.const import TOOL_TYPE_MAPPING, \ MODEL_CONFIG_MAPPING, CAN_EDIT_ALL_USER_ROLES, PERMISSION_PRIVATE from consts.exceptions import ( @@ -67,6 +68,7 @@ ) from database import skill_db from management.services.skill.service import SkillService +from database.agent_repository_db import list_agent_repository_by_agent_ids from database.agent_version_db import batch_search_version_names, query_version_list from database.group_db import query_group_ids_by_user from database.user_tenant_db import get_user_tenant_by_user_id @@ -922,6 +924,7 @@ async def list_all_agent_info_impl( async def list_agent_page_impl( tenant_id: str, user_id: str, + caller_tenant_id: Optional[str] = None, permission: Optional[str] = None, tag: Optional[str] = None, search: Optional[str] = None, @@ -932,6 +935,7 @@ async def list_agent_page_impl( created_by_not: Optional[str] = None, tag_predicates: Optional[list] = None, search_tag_predicates: Optional[list] = None, + include_repository_info: bool = False, ) -> Dict[str, Any]: """List visible agents with server-side filters and pagination.""" if created_by and created_by_not: @@ -1127,6 +1131,72 @@ async def list_agent_page_impl( ) agent["version_label"] = version.get("version_name") agent["version_create_time"] = version.get("create_time") + if include_repository_info: + for scope_tenant_id in tenant_ids: + scoped_agents = [ + agent for scope, agent in paged_scoped_agents + if scope == scope_tenant_id + ] + if not scoped_agents: + continue + scoped_agent_ids = [int(agent["agent_id"]) for agent in scoped_agents] + shared_records = list_agent_repository_by_agent_ids( + scoped_agent_ids, + statuses=(STATUS_SHARED,), + publisher_tenant_id=scope_tenant_id, + ) + publisher_records = [] + if scope_tenant_id == caller_tenant_id and user_role == "ADMIN": + publisher_records = list_agent_repository_by_agent_ids( + scoped_agent_ids, + statuses=(STATUS_PENDING_REVIEW, STATUS_REJECTED, STATUS_SHARED), + publisher_tenant_id=scope_tenant_id, + ) + elif scope_tenant_id == caller_tenant_id and user_role == "DEV": + publisher_records = list_agent_repository_by_agent_ids( + scoped_agent_ids, + statuses=(STATUS_PENDING_REVIEW, STATUS_REJECTED, STATUS_SHARED), + publisher_tenant_id=scope_tenant_id, + publisher_user_id=user_id, + ) + records_by_id = { + int(record["agent_repository_id"]): (record, False) + for record in shared_records + if record["status"] == STATUS_SHARED + } + records_by_id.update({ + int(record["agent_repository_id"]): (record, True) + for record in publisher_records + }) + repository_by_agent_id: dict[int, list[dict]] = {} + for record, is_publisher in records_by_id.values(): + created_at = record.get("create_time") + repository_by_agent_id.setdefault(int(record["agent_id"]), []).append( + { + "agent_repository_id": record["agent_repository_id"], + "status": record["status"], + "version_no": record["version_no"], + "version_label": record.get("version_name"), + "create_time": ( + created_at.isoformat() + if hasattr(created_at, "isoformat") + else created_at + ), + "content": record.get("content") if is_publisher else None, + } + ) + for records in repository_by_agent_id.values(): + records.sort( + key=lambda item: ( + str(item["create_time"] or ""), + int(item["agent_repository_id"]), + ), + reverse=True, + ) + for agent in scoped_agents: + agent["repository_info"] = repository_by_agent_id.get( + int(agent["agent_id"]), [] + ) paged_agents = [agent for _, agent in paged_scoped_agents] return { "items": paged_agents, diff --git a/frontend/app/[locale]/agent-space/components/MyAgentCard.tsx b/frontend/app/[locale]/agent-space/components/MyAgentCard.tsx index c997191b2b..7d8b8d64fc 100644 --- a/frontend/app/[locale]/agent-space/components/MyAgentCard.tsx +++ b/frontend/app/[locale]/agent-space/components/MyAgentCard.tsx @@ -1,9 +1,7 @@ "use client"; -import { Button, Dropdown, Spin, Tooltip } from "antd"; +import { Button, Dropdown, Tooltip } from "antd"; import type { MenuProps } from "antd"; -import { useState } from "react"; -import { useAgentRepositoryListings } from "@/hooks/agentRepository/useAgentRepositoryListings"; import { ClipboardCheck, Clock, @@ -21,7 +19,7 @@ import { getUnavailableReasonLabels } from "@/lib/agentLabelMapper"; import { formatMineDate, getMineCardMenuActions, - toMineRepositoryInfo, + getMineCardRepositoryStatusBadge, type MineCardMenuAction, } from "@/lib/agentRepositoryMine"; import type { MyEditableAgentItem } from "@/types/agentRepository"; @@ -69,16 +67,6 @@ export function MyAgentCard({ isDeleting = false, }: MyAgentCardProps) { const { t } = useTranslation("common"); - const [menuOpen, setMenuOpen] = useState(false); - const { - data: listingData, - isLoading: isListingLoading, - isError: isListingError, - refetch, - } = useAgentRepositoryListings( - { agent_id: agent.agent_id, page: 1, page_size: 100 }, - menuOpen || guideMenuOpen === true - ); const title = agent.name?.trim() || t("agentRepository.card.untitled"); const description = @@ -88,8 +76,6 @@ export function MyAgentCard({ agent.unavailable_reasons ?? [], t ); - const repositoryInfo = toMineRepositoryInfo(listingData?.items ?? []); - const agentWithRepository = { ...agent, repository_info: repositoryInfo }; const { canOpen: published } = getAgentUsageGuideAccess({ currentVersionNo: agent.current_version_no, permission: agent.permission, @@ -99,9 +85,10 @@ export function MyAgentCard({ const canEdit = agent.permission !== "READ_ONLY"; const canView = (agent.current_version_no ?? 0) > 0; const canEvaluate = canView; - const menuActions = listingData - ? getMineCardMenuActions(agentWithRepository) - : []; + const menuActions = getMineCardMenuActions(agent); + const repositoryBadge = getMineCardRepositoryStatusBadge( + agent.repository_info + ); const menuItems: MenuProps["items"] = menuActions.map((action) => { const icon = @@ -122,29 +109,13 @@ export function MyAgentCard({ return; } onViewReview( - agentWithRepository, + agent, action === "reviewUpdate" ? "reviewUpdate" : "review" ); }, }; }); - if (isListingLoading) { - menuItems.unshift({ - key: "loading", - label: , - disabled: true, - }); - } else if (isListingError) { - menuItems.unshift({ - key: "retry", - label: t("repository.common.retry"), - onClick: () => { - void refetch(); - }, - }); - } - if (canEvaluate) { menuItems.push({ key: "evaluate", @@ -223,7 +194,7 @@ export function MyAgentCard({ ))} @@ -888,7 +881,7 @@ function toMyAgentItem(agent: Agent): MyEditableAgentItem { version_create_time: agent.version_create_time ?? null, permission: agent.permission, tags: agent.tags, - repository_info: [], + repository_info: agent.repository_info ?? [], }; } diff --git a/frontend/app/[locale]/mcp-space/components/McpToolsSearchFilterBar.tsx b/frontend/app/[locale]/mcp-space/components/McpToolsSearchFilterBar.tsx index 83e9a04223..29b13a732c 100644 --- a/frontend/app/[locale]/mcp-space/components/McpToolsSearchFilterBar.tsx +++ b/frontend/app/[locale]/mcp-space/components/McpToolsSearchFilterBar.tsx @@ -72,13 +72,7 @@ export default function McpToolsSearchFilterBar({ }`} > {item.label} - - {item.count} - + {item.count} ); diff --git a/frontend/app/[locale]/mcp-space/components/MineMcpServiceCard.tsx b/frontend/app/[locale]/mcp-space/components/MineMcpServiceCard.tsx index 23c316872b..3e74165e36 100644 --- a/frontend/app/[locale]/mcp-space/components/MineMcpServiceCard.tsx +++ b/frontend/app/[locale]/mcp-space/components/MineMcpServiceCard.tsx @@ -304,7 +304,11 @@ export default function MineMcpServiceCard({ + ) : ( + + + {t("chat.sidebar.newConversation")} + + )} - + , + serverConversationIds?: ReadonlyMap +): string[] => { + const remoteIds = new Map( + threadItems.map((item) => [item.id, item.remoteId]) + ); + return threadIds.filter((id) => { + const conversationId = Number( + serverConversationIds?.get(id) || remoteIds.get(id) + ); + return Number.isInteger(conversationId) && conversationId > 0; + }); +}; diff --git a/frontend/app/[locale]/newchat/page.tsx b/frontend/app/[locale]/newchat/page.tsx index 5d705c7027..c5698da367 100644 --- a/frontend/app/[locale]/newchat/page.tsx +++ b/frontend/app/[locale]/newchat/page.tsx @@ -160,7 +160,6 @@ const PersistentChatHome: FC = () => { setSelectedAgent={setSelectedAgent} isLoadingAgents={isLoadingAgents} agents={agents} - onAgentSelected={handleAgentSelected} onBack={handleBack} isDictationConfigured={isDictationConfigured(modelConfig?.stt)} /> @@ -179,7 +178,6 @@ const HomeContent: FC<{ setSelectedAgent: (agent: Agent | null) => void; isLoadingAgents: boolean; agents: Agent[]; - onAgentSelected: (agent: Agent) => void; onBack: () => void; isDictationConfigured: boolean; }> = ({ @@ -188,7 +186,6 @@ const HomeContent: FC<{ setSelectedAgent, isLoadingAgents, agents, - onAgentSelected, onBack, isDictationConfigured, }) => { @@ -247,7 +244,9 @@ const HomeContent: FC<{ const [generatedTitles, setGeneratedTitles] = useState>( new Map() ); - const [, forceServerIdTick] = useState(0); + const [serverConversationIds, setServerConversationIds] = useState< + Map + >(new Map()); const handleServerConversationId = useCallback( (threadId: string, serverId: string, initialQuestion?: string) => { @@ -257,10 +256,8 @@ const HomeContent: FC<{ if (previous !== numericId) { map.set(threadId, numericId); cacheHistoricalChatMode(numericId, chatMode); - // Trigger a re-render so the `setRunConfig` effect below picks up the - // new id. We don't store the map in state because we never need to - // diff/render it directly — only react when an entry changes. - forceServerIdTick((tick) => tick + 1); + // Keep the sidebar and run configuration in sync with the server id. + setServerConversationIds(new Map(map)); } if (initialQuestion && previous !== numericId) { @@ -269,6 +266,7 @@ const HomeContent: FC<{ setGeneratedTitles((titles) => { const next = new Map(titles); next.set(threadId, title); + next.set(numericId, title); return next; }); }) @@ -726,8 +724,14 @@ const HomeContent: FC<{ const handleThreadBack = useCallback(async () => { shouldRestoreAgentRef.current = false; - await runtime.threads.switchToNewThread(); - onBack(); + try { + await runtime.threads.switchToNewThread(); + await runtime.threads.reload(); + } catch (error) { + log.error("[HomeContent] Failed to return to agent list:", error); + } finally { + onBack(); + } }, [onBack, runtime]); const handlePrepareNewConversation = useCallback(() => { @@ -737,22 +741,28 @@ const HomeContent: FC<{ }, [onBack]); const handleNewConversation = useCallback(async () => { - handlePrepareNewConversation(); - await runtime.threads.switchToNewThread(); - }, [handlePrepareNewConversation, runtime]); + shouldRestoreAgentRef.current = false; + try { + await runtime.threads.switchToNewThread(); + await runtime.threads.reload(); + } catch (error) { + log.error("[HomeContent] Failed to start a new conversation:", error); + } finally { + onBack(); + } + }, [onBack, runtime]); const handleAgentSelectedFromLanding = useCallback( async (agent: Agent) => { shouldRestoreAgentRef.current = true; - await runtime.threads.switchToNewThread(); const thread = runtime.threads.getItemById( runtime.threads.getState().mainThreadId ); await thread.initialize(); await thread.updateCustom({ agentId: agent.id }); - onAgentSelected(agent); + setSelectedAgent(agent); }, - [onAgentSelected, runtime] + [runtime, setSelectedAgent] ); // Conditional rendering must happen after all hooks @@ -770,6 +780,7 @@ const HomeContent: FC<{ diff --git a/frontend/app/[locale]/skill-space/components/MineSkillsView.tsx b/frontend/app/[locale]/skill-space/components/MineSkillsView.tsx index b7e195e8dd..6539820da7 100644 --- a/frontend/app/[locale]/skill-space/components/MineSkillsView.tsx +++ b/frontend/app/[locale]/skill-space/components/MineSkillsView.tsx @@ -418,6 +418,8 @@ const MINE_SKILL_STATUS_CLASS: Record = { }; function getMineSkillMenuItems({ + canApplyListing, + canEdit, canPublish, hasRepositoryInfo, t, @@ -425,6 +427,8 @@ function getMineSkillMenuItems({ onViewReview, onDelete, }: { + canApplyListing: boolean; + canEdit: boolean; canPublish: boolean; hasRepositoryInfo: boolean; t: (key: string) => string; @@ -433,7 +437,7 @@ function getMineSkillMenuItems({ onDelete: () => void; }): MenuProps["items"] { const items: MenuProps["items"] = []; - if (canPublish && !hasRepositoryInfo) { + if (canApplyListing) { items.push({ key: "apply-listing", label: t("skillRepository.mine.button.apply"), @@ -449,13 +453,15 @@ function getMineSkillMenuItems({ onClick: onViewReview, }); } - items.push({ - key: "delete", - label: t("common.delete"), - icon: , - danger: true, - onClick: onDelete, - }); + if (canEdit) { + items.push({ + key: "delete", + label: t("common.delete"), + icon: , + danger: true, + onClick: onDelete, + }); + } return items; } @@ -485,7 +491,10 @@ function MineSkillCard({ const canPublish = skill.can_publish === true; const updatedAt = formatRepositoryDate(skill.updated_at ?? skill.update_time); const tags = skill.tags?.filter((tag) => tag.trim()) ?? []; + const canApplyListing = canPublish && !hasRepositoryInfo; const menuItems = getMineSkillMenuItems({ + canApplyListing, + canEdit, canPublish, hasRepositoryInfo, t, @@ -498,6 +507,7 @@ function MineSkillCard({ @@ -531,7 +541,7 @@ function MineSkillCard({ footerLayout="inline" headerActions={
- {canEdit ? ( + {canEdit || canApplyListing ? (