diff --git a/ui/web/src/i18n/locales/en/agents.json b/ui/web/src/i18n/locales/en/agents.json index 5d7f0bbe..8ef6dcc6 100644 --- a/ui/web/src/i18n/locales/en/agents.json +++ b/ui/web/src/i18n/locales/en/agents.json @@ -724,7 +724,10 @@ "retrying": "Retrying...", "done": "done", "fileLabelSOUL": "Soul & Personality", - "fileLabelIDENTITY": "Identity Card" + "fileLabelIDENTITY": "Identity Card", + "takingTooLong": "Taking too long?", + "cancel": "Cancel", + "cancelling": "Cancelling..." }, "configGroups": { "core": "Core", diff --git a/ui/web/src/i18n/locales/vi/agents.json b/ui/web/src/i18n/locales/vi/agents.json index cec9d42a..34ecd77b 100644 --- a/ui/web/src/i18n/locales/vi/agents.json +++ b/ui/web/src/i18n/locales/vi/agents.json @@ -709,7 +709,10 @@ "retrying": "Đang thử lại...", "done": "xong", "fileLabelSOUL": "Linh hồn & Tính cách", - "fileLabelIDENTITY": "Thẻ danh tính" + "fileLabelIDENTITY": "Thẻ danh tính", + "takingTooLong": "Mất quá nhiều thời gian?", + "cancel": "Huỷ", + "cancelling": "Đang huỷ..." }, "configGroups": { "core": "Cốt lõi", diff --git a/ui/web/src/i18n/locales/zh/agents.json b/ui/web/src/i18n/locales/zh/agents.json index cb1470d8..59e4e86f 100644 --- a/ui/web/src/i18n/locales/zh/agents.json +++ b/ui/web/src/i18n/locales/zh/agents.json @@ -709,7 +709,10 @@ "retrying": "重试中...", "done": "完成", "fileLabelSOUL": "灵魂与个性", - "fileLabelIDENTITY": "身份卡" + "fileLabelIDENTITY": "身份卡", + "takingTooLong": "耗时过长?", + "cancel": "取消", + "cancelling": "正在取消..." }, "configGroups": { "core": "核心", diff --git a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx index dd2fd350..6692a235 100644 --- a/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx +++ b/ui/web/src/pages/agents/agent-detail/agent-detail-page.tsx @@ -35,7 +35,7 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { const navigate = useNavigate(); const { agent, files, loading, updateAgent, getFile, setFile, regenerateAgent, resummonAgent, refresh } = useAgentDetail(agentId); - const { deleteAgent: deleteAgentById } = useAgents(); + const { deleteAgent: deleteAgentById, cancelSummonAgent } = useAgents(); const hb = useAgentHeartbeat(agentId); const [summoningOpen, setSummoningOpen] = useState(false); const [activeTab, setActiveTab] = useState("agent"); @@ -161,6 +161,7 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) { agentName={title} onCompleted={() => {}} onResummon={async () => { await resummonAgent(); }} + onCancel={cancelSummonAgent} /> {heartbeatOpen && ( diff --git a/ui/web/src/pages/agents/agents-page.tsx b/ui/web/src/pages/agents/agents-page.tsx index 6f9ce2c7..700130ba 100644 --- a/ui/web/src/pages/agents/agents-page.tsx +++ b/ui/web/src/pages/agents/agents-page.tsx @@ -31,7 +31,7 @@ export function AgentsPage() { const { t } = useTranslation("agents"); const { id: detailId } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { agents, loading, createAgent, deleteAgent, refresh, resummonAgent } = useAgents(); + const { agents, loading, createAgent, deleteAgent, refresh, resummonAgent, cancelSummonAgent } = useAgents(); const showSkeleton = useDeferredLoading(loading && agents.length === 0); const [search, setSearch] = useState(""); @@ -288,6 +288,7 @@ export function AgentsPage() { agentName={summoningAgent.name} onCompleted={refresh} onResummon={resummonAgent} + onCancel={cancelSummonAgent} /> )} diff --git a/ui/web/src/pages/agents/hooks/use-agents.ts b/ui/web/src/pages/agents/hooks/use-agents.ts index a96660f7..7fc0f474 100644 --- a/ui/web/src/pages/agents/hooks/use-agents.ts +++ b/ui/web/src/pages/agents/hooks/use-agents.ts @@ -113,5 +113,12 @@ export function useAgents() { [http], ); - return { agents, loading, error, refresh: invalidate, createAgent, updateAgent, deleteAgent, resummonAgent }; + const cancelSummonAgent = useCallback( + async (id: string) => { + await http.post(`/v1/agents/${id}/cancel-summon`); + }, + [http], + ); + + return { agents, loading, error, refresh: invalidate, createAgent, updateAgent, deleteAgent, resummonAgent, cancelSummonAgent }; } diff --git a/ui/web/src/pages/agents/summoning-modal.tsx b/ui/web/src/pages/agents/summoning-modal.tsx index a76883a3..2f853895 100644 --- a/ui/web/src/pages/agents/summoning-modal.tsx +++ b/ui/web/src/pages/agents/summoning-modal.tsx @@ -17,10 +17,13 @@ interface SummoningModalProps { agentName: string; onCompleted: () => void; onResummon: (agentId: string) => Promise; + onCancel?: (agentId: string) => Promise; hideClose?: boolean; onContinue?: () => void; } +const CANCEL_THRESHOLD_SEC = 60; + const SUMMONING_FILES = [ { name: "SOUL.md", required: true }, { name: "IDENTITY.md", required: true }, @@ -33,6 +36,7 @@ export function SummoningModal({ agentName, onCompleted, onResummon, + onCancel, hideClose = false, onContinue, }: SummoningModalProps) { @@ -41,6 +45,7 @@ export function SummoningModal({ const [status, setStatus] = useState<"summoning" | "completed" | "failed">("summoning"); const [errorMsg, setErrorMsg] = useState(""); const [retrying, setRetrying] = useState(false); + const [cancelling, setCancelling] = useState(false); const [elapsed, setElapsed] = useState(0); const timerRef = useRef | null>(null); @@ -100,6 +105,19 @@ export function SummoningModal({ useWsEvent("agent.summoning", handleSummoningEvent); + const handleCancel = async () => { + if (!onCancel) return; + setCancelling(true); + try { + await onCancel(agentId); + // BE emits WS type=failed → existing handler closes modal + } catch { + // keep modal open + } finally { + setCancelling(false); + } + }; + const handleRetry = async () => { setRetrying(true); try { @@ -235,6 +253,15 @@ export function SummoningModal({

)} + {status === "summoning" && elapsed >= CANCEL_THRESHOLD_SEC && onCancel && ( +
+

{t("summoning.takingTooLong")}

+ +
+ )} + {status === "completed" && onContinue && (