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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user