feat(ui): add stop button for running traces (#481)

* feat(ui): add stop button for running traces on traces page

Allows admins to abort running agent runs directly from the traces page,
useful for stopping channel-originated runs (Telegram, Discord, etc.)
without needing access to the chat page.

* style(ui): match stop button style to chat destructive button

* fix(ui): fix abort response handling and variable declaration order

- Check aborted field from chat.abort response for accurate feedback
- Move useTraces() before handleAbortRun to fix block-scoped variable error
- Add abortNotFound i18n key for when run already finished

* fix(ui): remove invalid pending status check, add cursor-pointer
This commit is contained in:
Duc Nguyen authored and GitHub committed 2026-03-26 12:39:29 +07:00
1 parent 4d31fe0dda
commit 709d86c701
5 files changed
+77 -12

No files matched your search

+7 -2
View File
@@ -16,6 +16,7 @@
"time": "Time"
},
"unnamed": "Unnamed",
"stopRun": "Stop run",
"cached": "cached",
"detail": {
"title": "Trace Detail",
@@ -36,7 +37,8 @@
"spansCount": "Spans ({{count}})",
"llmCalls": "LLM",
"toolCalls": "tool",
"export": "Export trace"
"export": "Export trace",
"stopRun": "Stop run"
},
"span": {
"model": "Model:",
@@ -53,6 +55,9 @@
"cached": "cached"
},
"toast": {
"failedLoad": "Failed to load traces"
"failedLoad": "Failed to load traces",
"abortSent": "Run stopped",
"abortFailed": "Failed to stop run",
"abortNotFound": "Run already finished or not found"
}
}
+7 -2
View File
@@ -16,6 +16,7 @@
"time": "Thời gian"
},
"unnamed": "Không tên",
"stopRun": "Dừng chạy",
"cached": "đã cache",
"detail": {
"title": "Chi tiết theo dõi",
@@ -36,7 +37,8 @@
"spansCount": "Khoảng ({{count}})",
"llmCalls": "LLM",
"toolCalls": "công cụ",
"export": "Xuất trace"
"export": "Xuất trace",
"stopRun": "Dừng chạy"
},
"span": {
"model": "Model:",
@@ -53,6 +55,9 @@
"cached": "đã cache"
},
"toast": {
"failedLoad": "Không thể tải dữ liệu theo dõi"
"failedLoad": "Không thể tải dữ liệu theo dõi",
"abortSent": "Đã dừng chạy",
"abortFailed": "Không thể dừng chạy",
"abortNotFound": "Lệnh đã hoàn tất hoặc không tìm thấy"
}
}
+8 -3
View File
@@ -28,7 +28,8 @@
"title": "追踪详情",
"tokens": "令牌:",
"toolCalls": "工具",
"export": "导出追踪"
"export": "导出追踪",
"stopRun": "停止运行"
},
"emptyDescription": "暂无追踪记录。当Agent处理请求时将自动记录追踪。",
"emptyTitle": "暂无追踪",
@@ -51,7 +52,11 @@
},
"title": "追踪",
"toast": {
"failedLoad": "加载追踪失败"
"failedLoad": "加载追踪失败",
"abortSent": "已停止运行",
"abortFailed": "停止运行失败",
"abortNotFound": "运行已完成或未找到"
},
"unnamed": "未命名"
"unnamed": "未命名",
"stopRun": "停止运行"
}
@@ -7,7 +7,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { ChevronRight, ChevronDown, Copy, Check, Download, CircleCheck, CircleX, Loader, CircleMinus } from "lucide-react";
import { ChevronRight, ChevronDown, Copy, Check, Download, CircleCheck, CircleX, Loader, CircleMinus, Square } from "lucide-react";
import { useClipboard } from "@/hooks/use-clipboard";
import { useHttp } from "@/hooks/use-ws";
import { useWsEvent } from "@/hooks/use-ws-event";
@@ -53,9 +53,10 @@ interface TraceDetailDialogProps {
onClose: () => void;
getTrace: (id: string) => Promise<{ trace: TraceData; spans: SpanData[] } | null>;
onNavigateTrace?: (traceId: string) => void;
onAbortRun?: (trace: TraceData, e: React.MouseEvent) => void;
}
export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace }: TraceDetailDialogProps) {
export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace, onAbortRun }: TraceDetailDialogProps) {
const { t } = useTranslation("traces");
const tz = useUiStore((s) => s.timezone);
const http = useHttp();
@@ -160,6 +161,16 @@ export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace
)}
{t("detail.export")}
</button>
{trace && (trace.status === "running") && onAbortRun && (
<button
type="button"
onClick={(e) => onAbortRun(trace, e)}
className="flex cursor-pointer items-center gap-1 rounded-md bg-destructive px-2 py-1 text-xs text-destructive-foreground transition-colors hover:bg-destructive/90"
>
<Square className="h-3.5 w-3.5" />
{t("detail.stopRun")}
</button>
)}
</DialogTitle>
</DialogHeader>
+42 -3
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Activity, GitFork, RefreshCw } from "lucide-react";
import { Activity, GitFork, RefreshCw, Square } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
@@ -22,6 +22,9 @@ import { useDeferredLoading } from "@/hooks/use-deferred-loading";
import { useUiStore } from "@/stores/use-ui-store";
import { useAgents } from "@/pages/agents/hooks/use-agents";
import { useChannelInstances } from "@/pages/channels/hooks/use-channel-instances";
import { useWs } from "@/hooks/use-ws";
import { Methods } from "@/api/protocol";
import { toast } from "@/stores/use-toast-store";
export function TracesPage() {
const { t } = useTranslation("traces");
@@ -33,6 +36,7 @@ export function TracesPage() {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const ws = useWs();
const { agents } = useAgents();
const { instances: channels } = useChannelInstances();
@@ -42,6 +46,28 @@ export function TracesPage() {
limit: pageSize,
offset: (page - 1) * pageSize,
});
const handleAbortRun = useCallback(
async (trace: TraceData, e: React.MouseEvent) => {
e.stopPropagation(); // Don't open trace detail
if (!ws.isConnected) return;
try {
const res = await ws.call(Methods.CHAT_ABORT, {
sessionKey: trace.session_key,
runId: trace.run_id,
}) as { aborted?: boolean };
if (res?.aborted) {
toast.success(t("toast.abortSent"));
refresh();
} else {
toast.info(t("toast.abortNotFound"));
}
} catch {
toast.error(t("toast.abortFailed"));
}
},
[ws, t, refresh],
);
const spinning = useMinLoading(fetching);
const showSkeleton = useDeferredLoading(loading && traces.length === 0);
@@ -134,7 +160,19 @@ export function TracesPage() {
)}
</td>
<td className="px-4 py-3">
<StatusBadge status={trace.status} />
<div className="flex items-center gap-1.5">
<StatusBadge status={trace.status} />
{(trace.status === "running") && (
<Button
variant="destructive"
size="icon-xs"
onClick={(e) => handleAbortRun(trace, e)}
title={t("stopRun")}
>
<Square className="h-3 w-3" />
</Button>
)}
</div>
</td>
<td className="px-4 py-3 text-muted-foreground">
{formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))}
@@ -175,6 +213,7 @@ export function TracesPage() {
onClose={() => setSelectedTraceId(null)}
getTrace={getTrace}
onNavigateTrace={setSelectedTraceId}
onAbortRun={handleAbortRun}
/>
)}
</div>