fix(workspace): control history rendering and preview sizing

This commit is contained in:
肖应宇 2026-03-31 11:32:46 +08:00
parent b30cbd2a3b
commit ffd10063a9
6 changed files with 65 additions and 28 deletions

1
.gitignore vendored
View File

@ -50,3 +50,4 @@ frontend/imports
# ignore the legacy `web` folder
web/
memo.md
.codex

View File

@ -57,6 +57,12 @@ export default function ChatPage() {
} = useArtifacts();
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
const searchParams = useSearchParams();
// History render rules:
// - /workspace/chats/{thread_id}: always render history
// - /workspace/chats/new: render history only when xclaw_used=true
const shouldRenderHistory =
!isNewThread ||
searchParams.get("xclaw_used")?.trim().toLowerCase() === "true";
// Submission strategy:
// - xclaw_used=true: follow `isnew` (isnew=false => reuse existing thread)
@ -65,6 +71,13 @@ export default function ChatPage() {
if (!isNewThread) {
return false;
}
const queryThreadId = searchParams.get("thread_id")?.trim();
const reuseExistingThread =
!!queryThreadId &&
searchParams.get("isnew")?.trim().toLowerCase() === "false";
if (reuseExistingThread) {
return false;
}
if (searchParams.get("xclaw_used")?.trim().toLowerCase() !== "true") {
return true;
}
@ -115,6 +128,22 @@ export default function ChatPage() {
const [hasSubmitted, setHasSubmitted] = useState(false);
const showInputBox = !(isNewThread && thread.isThreadLoading);
const [historyCutoff, setHistoryCutoff] = useState<number | null>(null);
useEffect(() => {
if (shouldRenderHistory) {
setHistoryCutoff(null);
return;
}
if (historyCutoff === null && !thread.isThreadLoading) {
setHistoryCutoff(thread.messages.length);
}
}, [
historyCutoff,
shouldRenderHistory,
thread.isThreadLoading,
thread.messages.length,
]);
useEffect(() => {
const pageTitle = isNewThread
@ -231,7 +260,7 @@ export default function ChatPage() {
)}
</div>
<div className="flex items-center justify-end gap-2 overflow-hidden">
<TokenUsageIndicator messages={thread.messages} />
{/* <TokenUsageIndicator messages={thread.messages} /> */}
<DevTodoList
className="bg-white"
todos={thread.values.todos ?? []}
@ -280,6 +309,11 @@ export default function ChatPage() {
)}
threadId={threadId}
thread={thread}
messagesOverride={
shouldRenderHistory || historyCutoff === null
? undefined
: thread.messages.slice(historyCutoff)
}
paddingBottom={todoListCollapsed ? 160 : 280}
/>
</div>

View File

@ -142,6 +142,6 @@ export const ArtifactContent = ({
}: ArtifactContentProps) => (
<div className="min-h-0 flex-1 overflow-auto rounded-[10px]">
{/* <div className={cn("mb-[207px]! p-4", className)} {...props} /> */}
<div className={cn("mb-[150px] h-full p-4", className)} {...props} />
<div className={cn("mb-[150px] min-h-full p-4", className)} {...props} />
</div>
);

View File

@ -527,11 +527,11 @@ export function ArtifactFilePreview({
if (language === "markdown") {
return (
<div
className={cn("size-full p-[20px]")}
className={cn("w-full p-[20px]")}
style={{ "--zoom-scale": zoomScale } as React.CSSProperties}
>
<Streamdown
className="size-full"
className="w-full"
{...streamdownPlugins}
components={{ a: CitationLink }}
>

View File

@ -3,22 +3,22 @@
import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { uuid } from "@/core/utils/uuid";
export function useThreadChat() {
const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
const pathname = usePathname();
const searchParams = useSearchParams();
const xClawUsedFromQuery = searchParams.get("xclaw_used");
const isNewFromQuery =
searchParams.get("isnew")?.trim().toLowerCase() === "false";
const queryThreadIdFromParams = searchParams.get("thread_id")?.trim();
const shouldUseQueryThreadId =
pathname.startsWith("/workspace/chats/") &&
!!queryThreadIdFromParams &&
(xClawUsedFromQuery === "true" || isNewFromQuery);
const [threadId, setThreadId] = useState(() => {
if (threadIdFromPath === "new") {
const shouldUseQueryThreadId = pathname.startsWith("/workspace/chats/");
const queryThreadId =
shouldUseQueryThreadId && xClawUsedFromQuery === "true"
? searchParams.get("thread_id")?.trim()
: undefined;
return queryThreadId ?? uuid();
return shouldUseQueryThreadId ? queryThreadIdFromParams : undefined;
}
return threadIdFromPath;
});
@ -30,12 +30,15 @@ export function useThreadChat() {
useEffect(() => {
if (pathname.endsWith("/new")) {
setIsNewThread(true);
const shouldUseQueryThreadId = pathname.startsWith("/workspace/chats/");
const queryThreadId =
shouldUseQueryThreadId && xClawUsedFromQuery === "true"
? searchParams.get("thread_id")?.trim()
: undefined;
setThreadId(queryThreadId ?? uuid());
const nextQueryThreadId = searchParams.get("thread_id")?.trim();
const nextIsNewFromQuery =
searchParams.get("isnew")?.trim().toLowerCase() === "false";
const nextXClawUsed = searchParams.get("xclaw_used");
const nextShouldUseQueryThreadId =
pathname.startsWith("/workspace/chats/") &&
!!nextQueryThreadId &&
(nextXClawUsed === "true" || nextIsNewFromQuery);
setThreadId(nextShouldUseQueryThreadId ? nextQueryThreadId : undefined);
return;
}
setIsNewThread(false);

View File

@ -42,7 +42,6 @@ export function useIframeSkill(): UseIframeSkillReturn {
// 0. 监听 query 中 XClawUsed=true 且带 thread_id 时跳转并清理 query
useEffect(() => {
console.log(xClawUsedFromQuery, threadIdFromQuery, lastThreadIdRef.current);
if (!threadIdFromQuery) return;
if (xClawUsedFromQuery !== "true") return;