From febfae6251ea29216e295d22cbdac8b742f2e1b5 Mon Sep 17 00:00:00 2001 From: xiaofei Date: Tue, 3 Nov 2026 04:39:56 -0800 Subject: [PATCH] fix(chat): edit-resend rewrites the current conversation instead of branching Editing a sent message and resending used to fork a new K-brain session before running, leaving a near-duplicate conversation in the sidebar each time. Resend now rewrites the open conversation in place: the send preflight already calls K-brain's /edit endpoint, which truncates history after the edited message and replaces it before the run starts. - Drop the branching prepareEditResend step from ChatPage, useEditResend and the gateway edit_resend path. - Remove the now-unused "resent in a new branch" toast strings. - Explicit "branch conversation" from the message menu is unchanged. --- crates/agent-gui/src/pages/ChatPage.tsx | 81 +------------------ .../pages/chat/gateway/gatewayBridgeTypes.ts | 6 -- .../chat/gateway/useGatewayBridgeListeners.ts | 15 +--- .../src/pages/chat/hooks/useEditResend.ts | 16 +--- .../test/chat/edit-resend-atomic.test.mjs | 37 +-------- .../src/i18n/translations/enUSCommon.ts | 2 - .../src/i18n/translations/zhCNCommon.ts | 2 - 7 files changed, 8 insertions(+), 151 deletions(-) diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index e37729fec..a52212916 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -22,7 +22,6 @@ import { import { ThemeToggleButton } from "@liveagent/ui/components/ThemeToggleButton"; import { useConfirmDialog } from "@liveagent/ui/components/ui/confirm-dialog"; import { SidebarProvider, useSidebar } from "@liveagent/ui/components/ui/sidebar"; -import { toast } from "@liveagent/ui/components/ui/toast-manager"; import { PaneChrome } from "@liveagent/ui/components/workbench/PaneChrome"; import { type ProjectToolPaneEnvironment, @@ -137,12 +136,7 @@ import { createConversationStateFromContext, type RenderTimelineItem, } from "../lib/chat/conversation/conversationState"; -import { - buildConversationStateFromWindow, - CHAT_HISTORY_WINDOW_MESSAGES, - type ChatHistorySummary, - getChatHistoryWindow, -} from "../lib/chat/history/chatHistory"; +import type { ChatHistorySummary } from "../lib/chat/history/chatHistory"; import { buildFallbackConversationTitle, createConversationIdentity, @@ -216,7 +210,6 @@ import { useConversationPaneHostBridge } from "./chat/conversations/useConversat import { useConversationRuntimeEntrySnapshot } from "./chat/conversations/useConversationRuntimeEntrySnapshot"; import type { EnsureGatewayBridgeConversationReadyOptions, - PrepareEditResend, SendChatAction, } from "./chat/gateway/gatewayBridgeTypes"; import { useGatewayBridgeListeners } from "./chat/gateway/useGatewayBridgeListeners"; @@ -224,10 +217,7 @@ import { useGatewayBridgeReadiness } from "./chat/gateway/useGatewayBridgeReadin import { useGatewayRunMirrorCoordinator } from "./chat/gateway/useGatewayRunMirrorCoordinator"; import { useGatewayStatus } from "./chat/gateway/useGatewayStatus"; import { useBranchConversation } from "./chat/history/useBranchConversation"; -import { - branchConversationWithReload, - useConversationHistoryActions, -} from "./chat/history/useConversationHistoryActions"; +import { useConversationHistoryActions } from "./chat/history/useConversationHistoryActions"; import { useSharedHistory } from "./chat/history/useSharedHistory"; import { useChatPageRuntimeStore } from "./chat/hooks/useChatPageRuntimeStore"; import { @@ -249,7 +239,6 @@ import { useChatTurnQueue } from "./chat/queue/useChatTurnQueue"; import { createChatRuntimeHost } from "./chat/runtime/ChatRuntimeHost"; import { pruneIdleConversationRuntimeCaches, - setConversationRuntimeCacheEntry, syncMovedConversationRuntimeWorkdir, } from "./chat/runtime/chatPageRuntime"; import { createGuiClarifyRunner } from "./chat/runtime/clarifyRunner"; @@ -1579,70 +1568,6 @@ function ChatPageContent(props: ChatPageProps) { setContext(currentRequestContext); }, [currentRequestContext, setContext]); - const prepareEditResend = useCallback( - async (requestedConversationId, messageRef, options) => { - const sourceConversationId = - requestedConversationId.trim() || currentConversationIdRef.current.trim(); - if (!sourceConversationId) throw new Error("当前会话不存在,无法编辑重发"); - if (!kBrainBackendEnabled) return { conversationId: sourceConversationId, messageRef }; - const summary = await branchConversationWithReload( - sourceConversationId, - messageRef, - reloadConversation, - ); - sidebarStore.upsertLocal({ ...summary, isPending: undefined }); - const childWindow = await getChatHistoryWindow({ - id: summary.id, - maxMessages: CHAT_HISTORY_WINDOW_MESSAGES, - includeActiveSegment: true, - }); - if (!childWindow.activeSegment) throw new Error("编辑重发分支缺少活跃历史"); - const childState = buildConversationStateFromWindow(childWindow); - const childRef = childState.transcript.items - .filter((item): item is Extract => item.kind === "user") - .find((item) => item.messageRef?.messageId === messageRef.messageId)?.messageRef; - if (!childRef) throw new Error("编辑重发分支缺少稳定消息标识"); - const sourceEntry = conversationRuntimeCacheRef.current.get(sourceConversationId); - if (!sourceEntry) throw new Error("当前会话运行状态不存在,无法编辑重发"); - const childEntry = { - ...sourceEntry, - state: childState, - sessionId: childWindow.conversation.sessionId ?? childWindow.conversation.id, - createdAt: childWindow.conversation.createdAt, - workdir: childWindow.conversation.cwd, - isSending: false, - errorMessage: null, - }; - setConversationRuntimeCacheEntry(conversationRuntimeCacheRef.current, summary.id, childEntry); - conversationPersistenceCursorRef.current.set(summary.id, { - activeSegmentIndex: childWindow.activeSegment?.segmentIndex ?? 0, - activeSegmentId: childWindow.activeSegment?.segmentId ?? `kbrain:${summary.sessionId}`, - }); - if (options?.activate !== false) { - // Activate the child before its run starts. The source session remains - // available in the sidebar and is never edited. - await openInitialActionRef.current(summary.id); - toast.success(t("chat.editResendBranched"), { - id: `edit-resend-branch:${summary.id}`, - action: { - label: t("chat.editResendOpenSource"), - onClick: () => void openInitialActionRef.current(sourceConversationId), - }, - }); - } - return { conversationId: summary.id, messageRef: childRef }; - }, - [ - conversationPersistenceCursorRef, - conversationRuntimeCacheRef, - currentConversationIdRef, - kBrainBackendEnabled, - reloadConversation, - sidebarStore, - t, - ], - ); - const { send } = useSendChatTurn({ settings, workspaceProjects, @@ -1709,7 +1634,6 @@ function ChatPageContent(props: ChatPageProps) { currentConversationIdRef, conversationRuntimeCacheRef, ensureGatewayBridgeConversationReadyRef, - prepareEditResend, sendActionRef, queueGatewayBridgeEventForRequest, shouldQueueGatewayChatRequest, @@ -2373,7 +2297,6 @@ function ChatPageContent(props: ChatPageProps) { isConversationHydrationFailed, currentConversationIdRef, onError: handleEditResendError, - prepareEditResend, sendActionRef, }); diff --git a/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts b/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts index 792a24544..01de38f0a 100644 --- a/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts +++ b/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts @@ -66,12 +66,6 @@ export type EnsureGatewayBridgeConversationReadyOptions = { rebased?: boolean; }; -export type PrepareEditResend = ( - conversationId: string, - messageRef: HistoryMessageRef, - options?: { activate?: boolean }, -) => Promise<{ conversationId: string; messageRef: HistoryMessageRef }>; - export type GatewayChatCancelEvent = { requestId: string; conversationId: string; diff --git a/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts b/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts index 5bef1c848..0819db25b 100644 --- a/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts +++ b/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts @@ -24,13 +24,11 @@ import { normalizeGatewayCommandSafetyMode, normalizeGatewayExecutionMode, normalizeGatewayWorkdir, - type PrepareEditResend, } from "./gatewayBridgeTypes"; import { runGatewayComposerAction } from "./gatewayComposerActions"; import { runGatewayHistoryAction } from "./gatewayHistoryActions"; type UseGatewayBridgeListenersParams = GatewayBridgeRuntimeRefs & { - prepareEditResend?: PrepareEditResend; queueGatewayBridgeEventForRequest: ( requestId: string, event: Record, @@ -479,7 +477,7 @@ export function useGatewayBridgeListeners(params: UseGatewayBridgeListenersParam failClaimedRequest(requestId, targetConversationId, "invalid_chat_command", message); return; } - let editResendBaseMessageRef = baseMessageRef; + const editResendBaseMessageRef = baseMessageRef; if (targetConversationId && payload.rebased !== true) { const running = Boolean( @@ -517,17 +515,6 @@ export function useGatewayBridgeListeners(params: UseGatewayBridgeListenersParam }, ); - const prepareEditResend = latestParamsRef.current.prepareEditResend; - if (payload.rebased === true && editResendBaseMessageRef && prepareEditResend) { - const prepared = await prepareEditResend( - resolvedConversationId, - editResendBaseMessageRef, - { activate: false }, - ); - resolvedConversationId = prepared.conversationId; - editResendBaseMessageRef = prepared.messageRef; - } - const runningRequest = getActiveGatewayBridgeRequestByConversationId(resolvedConversationId) || (clientRequestId diff --git a/crates/agent-gui/src/pages/chat/hooks/useEditResend.ts b/crates/agent-gui/src/pages/chat/hooks/useEditResend.ts index 90f657787..e2191f7a5 100644 --- a/crates/agent-gui/src/pages/chat/hooks/useEditResend.ts +++ b/crates/agent-gui/src/pages/chat/hooks/useEditResend.ts @@ -12,11 +12,6 @@ type UseEditResendParams = { currentConversationIdRef: MutableRefObject; onError: (error: unknown) => void; sendActionRef: MutableRefObject; - prepareEditResend?: ( - conversationId: string, - messageRef: HistoryMessageRef, - options?: { activate?: boolean }, - ) => Promise<{ conversationId: string; messageRef: HistoryMessageRef }>; }; export function useEditResend(params: UseEditResendParams) { @@ -27,7 +22,6 @@ export function useEditResend(params: UseEditResendParams) { currentConversationIdRef, onError, sendActionRef, - prepareEditResend, } = params; const editResendInFlightRef = useRef(false); @@ -53,15 +47,14 @@ export function useEditResend(params: UseEditResendParams) { if (!conversationId) return; editResendInFlightRef.current = true; try { - const prepared = prepareEditResend - ? await prepareEditResend(conversationId, messageRef) - : { conversationId, messageRef }; + // Edit-resend rewrites the current conversation in place: the send + // preflight replaces history from messageRef before the run starts. const accepted = await sendActionRef.current({ textOverride: normalized, composerDraftOverride: createTextComposerDraft(normalized, referencedConversations), uploadedFilesOverride: uploadedFiles, - conversationIdOverride: prepared.conversationId, - editResendBaseMessageRef: prepared.messageRef, + conversationIdOverride: conversationId, + editResendBaseMessageRef: messageRef, }); if (!accepted) throw new Error("编辑重发未启动,原历史保持不变。"); } catch (error) { @@ -76,7 +69,6 @@ export function useEditResend(params: UseEditResendParams) { isConversationHydrating, isSending, onError, - prepareEditResend, sendActionRef, ], ); diff --git a/crates/agent-gui/test/chat/edit-resend-atomic.test.mjs b/crates/agent-gui/test/chat/edit-resend-atomic.test.mjs index 60bd2de3e..0217a1454 100644 --- a/crates/agent-gui/test/chat/edit-resend-atomic.test.mjs +++ b/crates/agent-gui/test/chat/edit-resend-atomic.test.mjs @@ -34,7 +34,7 @@ const sendSource = fs.readFileSync( "utf8", ); -test("edit-resend delegates the replacement anchor to the send preflight", async () => { +test("edit-resend rewrites the current conversation in place via the send preflight", async () => { const calls = []; const errors = []; const useEditResend = loadUseEditResend(); @@ -83,41 +83,6 @@ test("edit-resend delegates the replacement anchor to the send preflight", async assert.deepEqual(calls[0].composerDraftOverride.conversationMentions, [referencedConversation]); }); -test("edit-resend forwards the prepared child conversation and anchor", async () => { - const calls = []; - const prepareCalls = []; - const prepareConversationIds = []; - const useEditResend = loadUseEditResend(); - const { handleResendFromEdit } = useEditResend({ - isSending: false, - isConversationHydrating: false, - isConversationHydrationFailed: false, - currentConversationIdRef: { current: "source-conversation" }, - onError: assert.fail, - prepareEditResend: async (conversationId, ref) => { - prepareConversationIds.push(conversationId); - prepareCalls.push(ref); - return { - conversationId: "child-conversation", - messageRef: { ...ref, messageId: "child-user", segmentId: "kbrain:child" }, - }; - }, - sendActionRef: { - current: async (input) => { - calls.push(input); - return true; - }, - }, - }); - - await handleResendFromEdit(messageRef, "edited prompt", [], []); - - assert.deepEqual(prepareConversationIds, ["source-conversation"]); - assert.deepEqual(prepareCalls, [messageRef]); - assert.equal(calls[0].conversationIdOverride, "child-conversation"); - assert.equal(calls[0].editResendBaseMessageRef.messageId, "child-user"); -}); - test("edit-resend does not authorize a handwritten conversation token", async () => { const calls = []; const useEditResend = loadUseEditResend(); diff --git a/crates/agent-ui/src/i18n/translations/enUSCommon.ts b/crates/agent-ui/src/i18n/translations/enUSCommon.ts index 0667562ce..4ebac14b5 100644 --- a/crates/agent-ui/src/i18n/translations/enUSCommon.ts +++ b/crates/agent-ui/src/i18n/translations/enUSCommon.ts @@ -322,8 +322,6 @@ export const EN_US_COMMON_TRANSLATIONS = { "chat.conversationBranchFrom": "Branched from “{title}”", "chat.conversationBranchSourceMissing": "Branch; the source conversation is not in this list", "chat.conversationOpenBranchSource": "Open source conversation", - "chat.editResendBranched": "Resent in a new branch; the original conversation is unchanged", - "chat.editResendOpenSource": "View original", "chat.conversationRenameTitle": "Rename chat", "chat.conversationRenameHint": "Keep it short and recognizable", "chat.conversationRenameSave": "Save", diff --git a/crates/agent-ui/src/i18n/translations/zhCNCommon.ts b/crates/agent-ui/src/i18n/translations/zhCNCommon.ts index 14371db27..cb9b7cd56 100644 --- a/crates/agent-ui/src/i18n/translations/zhCNCommon.ts +++ b/crates/agent-ui/src/i18n/translations/zhCNCommon.ts @@ -295,8 +295,6 @@ export const ZH_CN_COMMON_TRANSLATIONS = { "chat.conversationBranchFrom": "分支自「{title}」", "chat.conversationBranchSourceMissing": "分支会话,原会话不在当前列表中", "chat.conversationOpenBranchSource": "打开原会话", - "chat.editResendBranched": "已在新分支中重发,原会话保持不变", - "chat.editResendOpenSource": "查看原会话", "chat.conversationRenameTitle": "重命名聊天", "chat.conversationRenameHint": "保持简短且易于识别", "chat.conversationRenameSave": "保存",