+
🧪 iframe 通信测试
+ {/* 场景 5:XClaw 使用状态 */}
+
+
+ ⑤ XClaw 使用状态
+
+
+
+
+
+
+
{/* 日志 */}
{log.length > 0 && (
diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx
index 55157fee..078df2ab 100644
--- a/frontend/src/components/workspace/input-box.tsx
+++ b/frontend/src/components/workspace/input-box.tsx
@@ -57,6 +57,7 @@ import {
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useI18n } from "@/core/i18n/hooks";
+import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
import { useModels } from "@/core/models/hooks";
import type { AgentThreadContext } from "@/core/threads";
import { useIframeSkill } from "@/hooks/use-iframe-skill";
@@ -211,9 +212,15 @@ export function InputBox({
return;
}
setIsFocused(false);
+ if (isNewThread) {
+ sendToParent({
+ type: POST_MESSAGE_TYPES.XCLAW_USED,
+ XClawUsed: true,
+ });
+ }
onSubmit?.(message);
},
- [onSubmit, onStop, status],
+ [isNewThread, onSubmit, onStop, status],
);
const requestFormSubmit = useCallback(() => {
diff --git a/frontend/src/core/iframe-messages.ts b/frontend/src/core/iframe-messages.ts
index 9a28b2f1..d77adb64 100644
--- a/frontend/src/core/iframe-messages.ts
+++ b/frontend/src/core/iframe-messages.ts
@@ -9,6 +9,8 @@
export const POST_MESSAGE_TYPES = {
// 全屏切换
FULLSCREEN: "fullscreen",
+ // XClaw 使用状态
+ XCLAW_USED: "XClawUsed",
// 选择预定义 skill
SELECT_SKILL: "selectSkill",
// 打开 skill 选择对话框
@@ -33,6 +35,11 @@ export interface FullscreenMessage {
fullscreen: boolean;
}
+export interface XClawUsedMessage {
+ type: typeof POST_MESSAGE_TYPES.XCLAW_USED;
+ XClawUsed: boolean;
+}
+
export interface SelectSkillMessage {
type: typeof POST_MESSAGE_TYPES.SELECT_SKILL;
skill_id: string;
@@ -51,8 +58,13 @@ export interface SelectedSkillMessage {
// 发送消息的辅助函数
export function sendToParent(
- message: FullscreenMessage | SelectSkillMessage | OpenSkillDialogMessage,
+ message:
+ | FullscreenMessage
+ | XClawUsedMessage
+ | SelectSkillMessage
+ | OpenSkillDialogMessage,
): void {
+ console.log("[iframe] sendToParent:", message);
if (window.parent !== window) {
window.parent.postMessage(message, "*");
}
diff --git a/frontend/src/hooks/use-iframe-skill.ts b/frontend/src/hooks/use-iframe-skill.ts
index 63cd5cee..481968e4 100644
--- a/frontend/src/hooks/use-iframe-skill.ts
+++ b/frontend/src/hooks/use-iframe-skill.ts
@@ -1,5 +1,5 @@
-import { useSearchParams } from "next/navigation";
-import { useState, useEffect, useCallback } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useState, useEffect, useCallback, useRef } from "react";
import {
POST_MESSAGE_TYPES,
@@ -23,9 +23,13 @@ interface UseIframeSkillReturn {
}
export function useIframeSkill(): UseIframeSkillReturn {
+ const router = useRouter();
const searchParams = useSearchParams();
const skillIdFromQuery = searchParams.get("skill_id");
const titleFromQuery = searchParams.get("title");
+ const threadIdFromQuery = searchParams.get("thread_id");
+ const xClawUsedFromQuery = searchParams.get("XClawUsed");
+ const lastThreadIdRef = useRef(null);
const [selectedSkill, setSelectedSkill] = useState(null);
@@ -36,6 +40,15 @@ export function useIframeSkill(): UseIframeSkillReturn {
}
}, [skillIdFromQuery, titleFromQuery]);
+ // 0. 监听 query 中 XClawUsed=true 且带 thread_id 时跳转并清理 query
+ useEffect(() => {
+ if (!threadIdFromQuery) return;
+ if (xClawUsedFromQuery !== "true") return;
+ if (lastThreadIdRef.current === threadIdFromQuery) return;
+ lastThreadIdRef.current = threadIdFromQuery;
+ router.replace(`/workspace/chats/${threadIdFromQuery}`);
+ }, [router, threadIdFromQuery, xClawUsedFromQuery]);
+
// 2. 监听宿主页 postMessage
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css
index 593623c9..fdbaba0a 100644
--- a/frontend/src/styles/globals.css
+++ b/frontend/src/styles/globals.css
@@ -452,4 +452,29 @@ p {
.cm-line {
font-size: calc(14px * var(--zoom-scale));
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.ͼ4s {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.cm-content {
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ box-sizing: border-box;
+}
+
+.cm-scroller {
+ min-width: 0;
+}
+
+.cm-editor {
+ overflow: hidden;
+ contain: paint;
}