feat(webui): add assistant reply fork-from-here

This commit is contained in:
Bayern4ever-dot
2026-06-10 04:26:06 +08:00
committed by Xubin Ren
parent 4a58b83acc
commit 03bca4c0a9
30 changed files with 1358 additions and 36 deletions
+42 -1
View File
@@ -77,6 +77,7 @@ interface ThreadShellProps {
onGoHome?: () => void;
onNewChat?: () => void;
onCreateChat?: (workspaceScope?: WorkspaceScopePayload | null) => Promise<string | null>;
onForkChat?: (sourceChatId: string, beforeUserIndex: number) => Promise<string | null>;
onTurnEnd?: () => void;
theme?: "light" | "dark";
onToggleTheme?: () => void;
@@ -226,6 +227,7 @@ export function ThreadShell({
title,
onToggleSidebar,
onCreateChat,
onForkChat,
onTurnEnd,
theme = "light",
onToggleTheme = () => {},
@@ -275,6 +277,8 @@ export function ThreadShell({
const [filePreviewPath, setFilePreviewPath] = useState<string | null>(null);
const [filePreviewClosing, setFilePreviewClosing] = useState(false);
const [filePreviewWidth, setFilePreviewWidth] = useState(FILE_PREVIEW_DEFAULT_WIDTH);
const [forkError, setForkError] = useState<string | null>(null);
const [forkHydratingChatId, setForkHydratingChatId] = useState<string | null>(null);
const shellRef = useRef<HTMLElement | null>(null);
const filePreviewWidthRef = useRef(FILE_PREVIEW_DEFAULT_WIDTH);
const filePreviewCloseTimerRef = useRef<number | null>(null);
@@ -283,6 +287,7 @@ export function ThreadShell({
const messageCacheRef = useRef<Map<string, UIMessage[]>>(new Map());
/** Last chatId we associated with the in-memory thread (for cache-on-switch). */
const prevChatIdForCacheRef = useRef<string | null>(null);
const prevChatIdForComposerRef = useRef<string | null>(chatId);
/** Skip one message-cache write right after chatId changes (messages may not match yet). */
const skipLayoutCacheRef = useRef(false);
const appliedHistoryVersionRef = useRef<Map<string, number>>(new Map());
@@ -334,6 +339,12 @@ export function ThreadShell({
};
}, []);
useEffect(() => {
if (prevChatIdForComposerRef.current === chatId) return;
prevChatIdForComposerRef.current = chatId;
setForkError(null);
}, [chatId]);
const displayMessages = useMemo(() => projectWebuiThreadMessages(messages), [messages]);
const showHeroComposer = messages.length === 0 && !loading;
@@ -443,6 +454,12 @@ export function ThreadShell({
setMessages(projectWebuiThreadMessages(historical));
}, [chatId, historical, setMessages]);
useEffect(() => {
if (!chatId || loading || forkHydratingChatId !== chatId) return;
setForkHydratingChatId(null);
setScrollToBottomSignal((value) => value + 1);
}, [chatId, forkHydratingChatId, loading]);
useLayoutEffect(() => {
if (chatId) {
const prev = prevChatIdForCacheRef.current;
@@ -521,6 +538,7 @@ export function ThreadShell({
const handleThreadSend = useCallback(
(content: string, images?: SendImage[], options?: SendOptions) => {
setForkError(null);
setScrollToBottomSignal((value) => value + 1);
send(content, images, withWorkspaceScope(options));
},
@@ -615,6 +633,26 @@ export function ThreadShell({
};
}, [filePreviewPath]);
const handleForkFromMessage = useCallback(
async (beforeUserIndex: number) => {
if (!chatId || !onForkChat) return;
setForkError(null);
const forkedChatId = await onForkChat(chatId, beforeUserIndex);
if (!forkedChatId) {
setForkError(t("thread.fork.failed", {
defaultValue: "Could not fork this chat. Try again.",
}));
return;
}
messageCacheRef.current.delete(forkedChatId);
appliedHistoryVersionRef.current.delete(forkedChatId);
pendingCanonicalHydrateRef.current.add(forkedChatId);
setForkHydratingChatId(forkedChatId);
setForkError(null);
},
[chatId, onForkChat, t],
);
const composer = (
<>
{streamError ? (
@@ -626,7 +664,7 @@ export function ThreadShell({
{session ? (
<ThreadComposer
onSend={handleThreadSend}
disabled={!chatId}
disabled={!chatId || forkHydratingChatId === chatId}
isStreaming={isStreaming}
placeholder={
showHeroComposer
@@ -653,6 +691,7 @@ export function ThreadShell({
workspaceError={workspaceError}
onWorkspaceScopeChange={onWorkspaceScopeChange}
pendingQueueKey={chatId}
externalError={forkError}
/>
) : (
<ThreadComposer
@@ -736,7 +775,9 @@ export function ThreadShell({
showScrollToBottomButton={!!session}
cliApps={cliApps}
mcpPresets={mcpPresets}
allMessages={displayMessages}
onOpenFilePreview={historyKey ? handleOpenFilePreview : undefined}
onForkFromMessage={onForkChat ? handleForkFromMessage : undefined}
/>
</div>
{filePreviewPath && historyKey ? (