fix: use custom event for training panel toggle, no more poll inference

SkillInsertDialog dispatches SKILL_TOGGLED_EVENT on user click.
AssistantWorkspace listens for that event to set applied state.
Completely eliminates poll-based state inference that caused flash.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
hupenglong1
2026-05-25 17:46:51 +08:00
parent 2acbd0b509
commit 5b708d2996
3 changed files with 24 additions and 24 deletions
+12 -24
View File
@@ -41,6 +41,8 @@ import { ClawSessionReplayProvider } from "@/lib/claw-session-replay";
import { pushSessionUrl } from "@/lib/claw-session-url"; import { pushSessionUrl } from "@/lib/claw-session-url";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
SKILL_TOGGLED_EVENT,
type SkillToggledDetail,
useAppliedTraining, useAppliedTraining,
useModelIterationEnabled, useModelIterationEnabled,
} from "@/lib/use-training-mode"; } from "@/lib/use-training-mode";
@@ -329,31 +331,17 @@ function AssistantWorkspace() {
closeActivityPanel(); closeActivityPanel();
}, [showPipeline, closeActivityPanel]); }, [showPipeline, closeActivityPanel]);
// Skill 开关联动 applied:只有用户在本次页面会话中「主动点击」开启 skill // 面板只通过用户在 SkillInsertDialog 里显式点击开/关 model-iteration 来控制。
// 才展开面板。页面加载时 skill 已经是 enabled 的情况不自动展开(避免闪烁) // 监听 SkillInsertDialog 发出的自定义事件,不再从 poll 状态变化推断
// 判断依据:skill.loading 从 true→false 是初始加载完成,之后 enabled 变化
// 才是用户主动操作。
const skillLoadedOnce = useRef(false);
const prevSkillEnabled = useRef<boolean | null>(null);
useEffect(() => { useEffect(() => {
if (skill.loading) return; const handler = (e: Event) => {
if (!skillLoadedOnce.current) { const detail = (e as CustomEvent<SkillToggledDetail>).detail;
skillLoadedOnce.current = true; if (detail.skill !== "model-iteration") return;
prevSkillEnabled.current = skill.enabled; setApplied(detail.enabled);
// 初始加载:无条件清除 applied(清 localStorage 残留),面板只从用户点击打开 };
if (applied) { window.addEventListener(SKILL_TOGGLED_EVENT, handler);
setApplied(false); return () => window.removeEventListener(SKILL_TOGGLED_EVENT, handler);
} }, [setApplied]);
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]);
// 注意:用同一棵树 + 条件渲染,保证 <Thread /> 和 <ActivityPanel /> 在 // 注意:用同一棵树 + 条件渲染,保证 <Thread /> 和 <ActivityPanel /> 在
// pipeline 切换前后都保持挂载,否则它们会 remount, // pipeline 切换前后都保持挂载,否则它们会 remount,
@@ -93,6 +93,7 @@ import {
} from "@/lib/claw-active-session"; } from "@/lib/claw-active-session";
import { useClawSessionReplay } from "@/lib/claw-session-replay"; import { useClawSessionReplay } from "@/lib/claw-session-replay";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { dispatchSkillToggled } from "@/lib/use-training-mode";
type ComposerInsertEvent = CustomEvent<{ text: string }>; type ComposerInsertEvent = CustomEvent<{ text: string }>;
@@ -1800,6 +1801,7 @@ function SkillInsertDialog() {
); );
} }
setSkills(payload); setSkills(payload);
dispatchSkillToggled({ skill: skill.name, enabled });
setStatus(enabled ? `已启用 ${skill.name}` : `已停用 ${skill.name}`); setStatus(enabled ? `已启用 ${skill.name}` : `已停用 ${skill.name}`);
} catch (err) { } catch (err) {
setSkills(previous); setSkills(previous);
+10
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
export const SESSION_UPDATED_EVENT = "claw-session-updated"; export const SESSION_UPDATED_EVENT = "claw-session-updated";
export const SKILL_TOGGLED_EVENT = "claw-skill-toggled";
const APPLIED_STORAGE_PREFIX = "claw.training.applied:"; const APPLIED_STORAGE_PREFIX = "claw.training.applied:";
const MODEL_ITERATION_SKILL_NAMES = new Set(["model-iteration"]); 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<SkillToggledDetail>(SKILL_TOGGLED_EVENT, { detail }),
);
}
export function useTrainingMode(sessionId: string | null) { export function useTrainingMode(sessionId: string | null) {
const [isTraining, setIsTraining] = useState(false); const [isTraining, setIsTraining] = useState(false);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);