diff --git a/frontend/app/app/assistant.tsx b/frontend/app/app/assistant.tsx index 813039c..8e3c00f 100644 --- a/frontend/app/app/assistant.tsx +++ b/frontend/app/app/assistant.tsx @@ -41,6 +41,8 @@ import { ClawSessionReplayProvider } from "@/lib/claw-session-replay"; import { pushSessionUrl } from "@/lib/claw-session-url"; import { cn } from "@/lib/utils"; import { + SKILL_TOGGLED_EVENT, + type SkillToggledDetail, useAppliedTraining, useModelIterationEnabled, } from "@/lib/use-training-mode"; @@ -329,31 +331,17 @@ function AssistantWorkspace() { closeActivityPanel(); }, [showPipeline, closeActivityPanel]); - // Skill 开关联动 applied:只有用户在本次页面会话中「主动点击」开启 skill - // 才展开面板。页面加载时 skill 已经是 enabled 的情况不自动展开(避免闪烁)。 - // 判断依据:skill.loading 从 true→false 是初始加载完成,之后 enabled 变化 - // 才是用户主动操作。 - const skillLoadedOnce = useRef(false); - const prevSkillEnabled = useRef(null); + // 面板只通过用户在 SkillInsertDialog 里显式点击开/关 model-iteration 来控制。 + // 监听 SkillInsertDialog 发出的自定义事件,不再从 poll 状态变化推断。 useEffect(() => { - if (skill.loading) return; - if (!skillLoadedOnce.current) { - skillLoadedOnce.current = true; - prevSkillEnabled.current = skill.enabled; - // 初始加载:无条件清除 applied(清 localStorage 残留),面板只从用户点击打开 - if (applied) { - setApplied(false); - } - return; - } - // 初始加载之后的变化 = 用户主动操作 - if (skill.enabled && !prevSkillEnabled.current) { - setApplied(true); - } else if (!skill.enabled && prevSkillEnabled.current) { - setApplied(false); - } - prevSkillEnabled.current = skill.enabled; - }, [skill.enabled, skill.loading, applied, setApplied]); + const handler = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (detail.skill !== "model-iteration") return; + setApplied(detail.enabled); + }; + window.addEventListener(SKILL_TOGGLED_EVENT, handler); + return () => window.removeEventListener(SKILL_TOGGLED_EVENT, handler); + }, [setApplied]); // 注意:用同一棵树 + 条件渲染,保证 在 // pipeline 切换前后都保持挂载,否则它们会 remount, diff --git a/frontend/app/components/assistant-ui/thread.tsx b/frontend/app/components/assistant-ui/thread.tsx index 5441ca7..7972a4f 100644 --- a/frontend/app/components/assistant-ui/thread.tsx +++ b/frontend/app/components/assistant-ui/thread.tsx @@ -93,6 +93,7 @@ import { } from "@/lib/claw-active-session"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; import { cn } from "@/lib/utils"; +import { dispatchSkillToggled } from "@/lib/use-training-mode"; type ComposerInsertEvent = CustomEvent<{ text: string }>; @@ -1800,6 +1801,7 @@ function SkillInsertDialog() { ); } setSkills(payload); + dispatchSkillToggled({ skill: skill.name, enabled }); setStatus(enabled ? `已启用 ${skill.name}` : `已停用 ${skill.name}`); } catch (err) { setSkills(previous); diff --git a/frontend/app/lib/use-training-mode.ts b/frontend/app/lib/use-training-mode.ts index c7b8ca6..6c3ccd3 100644 --- a/frontend/app/lib/use-training-mode.ts +++ b/frontend/app/lib/use-training-mode.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react"; export const SESSION_UPDATED_EVENT = "claw-session-updated"; +export const SKILL_TOGGLED_EVENT = "claw-skill-toggled"; const APPLIED_STORAGE_PREFIX = "claw.training.applied:"; const MODEL_ITERATION_SKILL_NAMES = new Set(["model-iteration"]); @@ -158,6 +159,15 @@ export function dispatchSessionUpdated(detail: SessionUpdatedDetail) { ); } +export type SkillToggledDetail = { skill: string; enabled: boolean }; + +export function dispatchSkillToggled(detail: SkillToggledDetail) { + if (typeof window === "undefined") return; + window.dispatchEvent( + new CustomEvent(SKILL_TOGGLED_EVENT, { detail }), + ); +} + export function useTrainingMode(sessionId: string | null) { const [isTraining, setIsTraining] = useState(false); const [loaded, setLoaded] = useState(false);