From 2a7a8f9a8484fa5c4578ca20b97c80d73da69f92 Mon Sep 17 00:00:00 2001
From: viettranx
Date: Fri, 17 Apr 2026 18:48:50 +0700
Subject: [PATCH] feat(web): add cancel-summon button in SummoningModal
Add Cancel button in SummoningModal after 60s threshold. Call POST
/v1/agents/{id}/cancel-summon via useHttp hook. Update agent status
to summon_failed. Add i18n strings for en/vi/zh in agents locale.
---
ui/web/src/i18n/locales/en/agents.json | 5 +++-
ui/web/src/i18n/locales/vi/agents.json | 5 +++-
ui/web/src/i18n/locales/zh/agents.json | 5 +++-
.../agents/agent-detail/agent-detail-page.tsx | 3 ++-
ui/web/src/pages/agents/agents-page.tsx | 3 ++-
ui/web/src/pages/agents/hooks/use-agents.ts | 9 ++++++-
ui/web/src/pages/agents/summoning-modal.tsx | 27 +++++++++++++++++++
ui/web/src/pages/setup/step-agent.tsx | 3 ++-
8 files changed, 53 insertions(+), 7 deletions(-)
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 && (