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.
This commit is contained in:
viettranx committed 2026-04-17 19:02:53 +07:00
1 parent d7342fbd6e
commit 2a7a8f9a84
8 files changed
+53 -7

No files matched your search

+4 -1
View File
@@ -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",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -709,7 +709,10 @@
"retrying": "重试中...",
"done": "完成",
"fileLabelSOUL": "灵魂与个性",
"fileLabelIDENTITY": "身份卡"
"fileLabelIDENTITY": "身份卡",
"takingTooLong": "耗时过长?",
"cancel": "取消",
"cancelling": "正在取消..."
},
"configGroups": {
"core": "核心",
@@ -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 && (
+2 -1
View File
@@ -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}
/>
)}
</div>
+8 -1
View File
@@ -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 };
}
@@ -17,10 +17,13 @@ interface SummoningModalProps {
agentName: string;
onCompleted: () => void;
onResummon: (agentId: string) => Promise<void>;
onCancel?: (agentId: string) => Promise<void>;
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<ReturnType<typeof setInterval> | 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({
</p>
)}
{status === "summoning" && elapsed >= CANCEL_THRESHOLD_SEC && onCancel && (
<div className="flex flex-col items-center gap-1">
<p className="text-xs text-muted-foreground">{t("summoning.takingTooLong")}</p>
<Button variant="ghost" size="sm" onClick={handleCancel} disabled={cancelling}>
{cancelling ? t("summoning.cancelling") : t("summoning.cancel")}
</Button>
</div>
)}
{status === "completed" && onContinue && (
<Button size="sm" onClick={onContinue}>
{t("summoning.continue")}
+2 -1
View File
@@ -26,7 +26,7 @@ interface StepAgentProps {
export function StepAgent({ provider, model, onComplete, onBack, existingAgent }: StepAgentProps) {
const { t } = useTranslation("setup");
const { createAgent, updateAgent, deleteAgent, resummonAgent } = useAgents();
const { createAgent, updateAgent, deleteAgent, resummonAgent, cancelSummonAgent } = useAgents();
const agentPresets = useAgentPresets();
const isEditing = !!existingAgent;
@@ -274,6 +274,7 @@ export function StepAgent({ provider, model, onComplete, onBack, existingAgent }
agentName={createdAgent.name}
onCompleted={handleSummoningComplete}
onResummon={resummonAgent}
onCancel={cancelSummonAgent}
hideClose
onContinue={summoningOutcome === "success" ? handleContinue : undefined}
/>