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
@@ -24,6 +24,7 @@ import {
import { Button } from "@/components/ui/button";
import {
clearActiveSessionId,
markFreshLocalId,
readActiveSessionId,
writeActiveSessionId,
writePendingWorkspaceSessionId,
@@ -163,6 +164,7 @@ function ensureSidebarWorkspaceSessionId(): {
typeof crypto !== "undefined" && "randomUUID" in crypto
? `__LOCALID_${crypto.randomUUID().replace(/-/g, "").slice(0, 8)}`
: `__LOCALID_${Math.random().toString(36).slice(2, 10)}`;
markFreshLocalId(generated);
writeActiveSessionId(generated);
return { sessionId: generated, created: true };
}
@@ -438,6 +440,10 @@ const ClawSessionList: FC = () => {
type="button"
className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center truncate px-3 text-start text-sm"
onClick={async () => {
console.log("[session-list] click", {
sessionId: session.session_id,
preview: session.preview,
});
setOpenMenuSessionId(null);
writeActiveSessionId(session.session_id);
pushSessionUrl(session.session_id);
+38 -19
View File
@@ -84,6 +84,7 @@ import {
ACTIVE_SESSION_CHANGED_EVENT,
clearActiveSessionId,
clearPendingWorkspaceSessionId,
markFreshLocalId,
PENDING_WORKSPACE_SESSION_CHANGED_EVENT,
readActiveSessionId,
readPendingWorkspaceSessionId,
@@ -261,8 +262,14 @@ function useRefreshCurrentRun() {
includePending: true,
includeActive: true,
});
const activeRunRef = useRef<string | null>(null);
const appliedTerminalRef = useRef<string | null>(null);
// Per-session bookkeeping: switching sessions (sidebar history) and coming
// back must NOT re-trigger replaySession on a session that has already been
// applied — replaySession rebuilds the thread tree, which causes a layout
// shift / scroll jitter in the chat UI. Single-value refs would get
// overwritten when visiting another session and forget that this session
// was already settled.
const activeRunRefMap = useRef<Map<string, string>>(new Map());
const appliedTerminalRefMap = useRef<Map<string, string>>(new Map());
useEffect(() => {
if (!sessionId) return;
@@ -274,8 +281,8 @@ function useRefreshCurrentRun() {
if (cancelled) return;
if (runStatus?.status === "queued" || runStatus?.status === "running") {
const runKey = runStatus.run_id ?? "active";
const previousRunKey = activeRunRef.current;
activeRunRef.current = runKey;
const previousRunKey = activeRunRefMap.current.get(sessionId) ?? null;
activeRunRefMap.current.set(sessionId, runKey);
// Watcher auto-resume: backend started a new run via _maybe_auto_resume
// without a local SSE stream driving the UI. We need to replay once so
// the active-run placeholder (pending prompt + 思考中 bubble) appears.
@@ -294,14 +301,15 @@ function useRefreshCurrentRun() {
}
return;
}
if (!runtimeRunning && !activeRunRef.current) return;
const sessionActiveRun = activeRunRefMap.current.get(sessionId) ?? null;
if (!runtimeRunning && !sessionActiveRun) return;
const terminalKey = [
runStatus?.run_id ?? activeRunRef.current ?? "unknown",
runStatus?.run_id ?? sessionActiveRun ?? "unknown",
runStatus?.status ?? "idle",
runStatus?.finished_at ?? "",
runStatus?.elapsed_ms ?? "",
].join(":");
if (appliedTerminalRef.current === terminalKey) return;
if (appliedTerminalRefMap.current.get(sessionId) === terminalKey) return;
const response = await fetch(`/api/claw/sessions/${sessionId}`, {
cache: "no-store",
});
@@ -311,8 +319,8 @@ function useRefreshCurrentRun() {
sessionId,
toReplayRepository(payload, sessionId, runStatus),
);
activeRunRef.current = null;
appliedTerminalRef.current = terminalKey;
activeRunRefMap.current.delete(sessionId);
appliedTerminalRefMap.current.set(sessionId, terminalKey);
scheduleSessionListRefresh();
}
@@ -775,6 +783,7 @@ function ensureWorkspaceSessionId(current: string | null) {
typeof crypto !== "undefined" && "randomUUID" in crypto
? `__LOCALID_${crypto.randomUUID().replace(/-/g, "").slice(0, 8)}`
: `__LOCALID_${Math.random().toString(36).slice(2, 10)}`;
markFreshLocalId(generated);
writeActiveSessionId(generated);
scheduleSessionListRefresh();
return generated;
@@ -2393,6 +2402,11 @@ const ImeComposerInput: FC<ComponentProps<typeof TextareaAutosize>> = ({
const justSubmittedRef = useRef(false);
const isDisabled = runtimeDisabled || disabled;
// Pull store→local on external store changes (session switch, paste-from-attachment,
// submit-clear, etc). The reverse direction (local→store) is pushed eagerly by
// onChange/handleInsert/handleKeyDown/onCompositionEnd via syncComposerText, so we
// don't need a symmetric effect — and adding one creates a feedback loop that
// re-renders the composer at ~1ms intervals (visible as shake).
useEffect(() => {
if (!isComposingRef.current) {
if (justSubmittedRef.current && storeText) return;
@@ -2401,16 +2415,6 @@ const ImeComposerInput: FC<ComponentProps<typeof TextareaAutosize>> = ({
}
}, [storeText]);
useEffect(() => {
if (
!isComposingRef.current &&
localText !== storeText &&
aui.composer().getState().isEditing
) {
aui.composer().setText(localText);
}
}, [aui, localText, storeText]);
useEffect(() => {
if (!autoFocus || isDisabled) return;
const textarea = textareaRef.current;
@@ -2433,6 +2437,21 @@ const ImeComposerInput: FC<ComponentProps<typeof TextareaAutosize>> = ({
};
}, [aui]);
// AssistantWorkspace 切 session 时通过事件通知我们恢复 draft——直接
// 控制 localText,绕过 store→local 路径上的 justSubmittedRef 屏蔽和
// switchToNewThread 引起的异步覆盖。
useEffect(() => {
const handler = (event: Event) => {
const detail = (event as CustomEvent<{ text?: string }>).detail;
const text = detail?.text ?? "";
justSubmittedRef.current = false;
setLocalText(text);
};
window.addEventListener("claw-composer-restore-draft", handler);
return () =>
window.removeEventListener("claw-composer-restore-draft", handler);
}, []);
const syncComposerText = useCallback(
(value: string) => {
if (!aui.composer().getState().isEditing) return;