backend/frontend/runtime: 训练面板 / session 隔离 / agent runtime 调整

- backend/api/server.py:训练 step-detail / pipeline 卡片排序逻辑
- frontend:thread-list / thread / training-pipeline-panel / step-detail-sheet 配套调整,新增 metrics-chart-panel
- src/agent_*:tool spec / runtime / prompting 配套
- .gitignore 加 .logs/

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
hupenglong1
2026-05-25 11:43:10 +08:00
parent 57f5b60a3e
commit f068311cac
17 changed files with 1571 additions and 270 deletions
@@ -3,10 +3,12 @@
import {
ExternalLinkIcon,
FileIcon,
Maximize2Icon,
Minimize2Icon,
RefreshCwIcon,
XIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Button } from "@/components/ui/button";
@@ -52,6 +54,7 @@ type StepDetail = {
type Props = {
sessionId: string | null;
stepKey: string | null;
runId?: string | null;
stepTitle?: string;
stepStatus?: string;
open: boolean;
@@ -61,6 +64,7 @@ type Props = {
export function StepDetailSheet({
sessionId,
stepKey,
runId,
stepTitle,
stepStatus,
open,
@@ -69,13 +73,41 @@ export function StepDetailSheet({
const [detail, setDetail] = useState<StepDetail | null>(null);
const [loading, setLoading] = useState(false);
const [reloadCount, setReloadCount] = useState(0);
const [width, setWidth] = useState(672);
const [isFullscreen, setIsFullscreen] = useState(false);
const dragRef = useRef<{ startX: number; startW: number } | null>(null);
const onResizePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (isFullscreen) return;
e.currentTarget.setPointerCapture(e.pointerId);
dragRef.current = { startX: e.clientX, startW: width };
};
const onResizePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (!dragRef.current) return;
const dx = dragRef.current.startX - e.clientX;
const max =
typeof window !== "undefined"
? Math.max(360, window.innerWidth - 32)
: 1200;
setWidth(Math.max(360, Math.min(max, dragRef.current.startW + dx)));
};
const onResizePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
if (!dragRef.current) return;
dragRef.current = null;
e.currentTarget.releasePointerCapture(e.pointerId);
};
useEffect(() => {
if (!open || !sessionId || !stepKey) return;
let cancelled = false;
setLoading(true);
setDetail(null);
const url = `/api/claw/training/step-detail?session_id=${encodeURIComponent(sessionId)}&step=${encodeURIComponent(stepKey)}`;
const params = new URLSearchParams({
session_id: sessionId,
step: stepKey,
});
if (runId) params.set("run_id", runId);
const url = `/api/claw/training/step-detail?${params.toString()}`;
fetch(url, { cache: "no-store" })
.then((res) => (res.ok ? res.json() : null))
.then((payload) => {
@@ -89,15 +121,39 @@ export function StepDetailSheet({
return () => {
cancelled = true;
};
}, [open, sessionId, stepKey, reloadCount]);
}, [open, sessionId, stepKey, runId, reloadCount]);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
hideClose
className="flex w-[min(50rem,calc(100vw-2rem))] flex-col gap-0 p-0 sm:max-w-2xl"
onEscapeKeyDown={(e) => {
if (isFullscreen) {
e.preventDefault();
setIsFullscreen(false);
}
}}
className="flex max-w-none flex-col gap-0 p-0 sm:max-w-none"
style={
isFullscreen
? { width: "100vw", maxWidth: "100vw" }
: { width: `${width}px`, maxWidth: "none" }
}
>
{!isFullscreen ? (
<div
onPointerDown={onResizePointerDown}
onPointerMove={onResizePointerMove}
onPointerUp={onResizePointerUp}
onPointerCancel={onResizePointerUp}
className="group absolute inset-y-0 left-0 z-50 w-1.5 cursor-ew-resize hover:bg-sky-500/40 active:bg-sky-500/60"
title="拖动调整宽度"
aria-label="拖动调整宽度"
>
<div className="-translate-y-1/2 absolute top-1/2 left-[2px] h-10 w-[2px] rounded-full bg-muted-foreground/30 group-hover:bg-sky-400" />
</div>
) : null}
<header className="flex shrink-0 items-start justify-between gap-3 border-b px-5 py-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
@@ -119,6 +175,21 @@ export function StepDetailSheet({
) : null}
</div>
<div className="mt-0.5 flex shrink-0 items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="size-7"
onClick={() => setIsFullscreen((v) => !v)}
title={isFullscreen ? "退出全屏 (Esc)" : "全屏"}
aria-label={isFullscreen ? "退出全屏" : "全屏"}
>
{isFullscreen ? (
<Minimize2Icon className="size-3.5" />
) : (
<Maximize2Icon className="size-3.5" />
)}
</Button>
<Button
type="button"
variant="ghost"