Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 2 additions & 79 deletions crates/agent-gui/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -216,18 +210,14 @@ 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";
import { useGatewayBridgeReadiness } from "./chat/gateway/useGatewayBridgeReadiness";
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 {
Expand All @@ -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";
Expand Down Expand Up @@ -1579,70 +1568,6 @@ function ChatPageContent(props: ChatPageProps) {
setContext(currentRequestContext);
}, [currentRequestContext, setContext]);

const prepareEditResend = useCallback<PrepareEditResend>(
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<typeof item, { kind: "user" }> => 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,
Expand Down Expand Up @@ -1709,7 +1634,6 @@ function ChatPageContent(props: ChatPageProps) {
currentConversationIdRef,
conversationRuntimeCacheRef,
ensureGatewayBridgeConversationReadyRef,
prepareEditResend,
sendActionRef,
queueGatewayBridgeEventForRequest,
shouldQueueGatewayChatRequest,
Expand Down Expand Up @@ -2373,7 +2297,6 @@ function ChatPageContent(props: ChatPageProps) {
isConversationHydrationFailed,
currentConversationIdRef,
onError: handleEditResendError,
prepareEditResend,
sendActionRef,
});

Expand Down
6 changes: 0 additions & 6 deletions crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
16 changes: 4 additions & 12 deletions crates/agent-gui/src/pages/chat/hooks/useEditResend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ type UseEditResendParams = {
currentConversationIdRef: MutableRefObject<string>;
onError: (error: unknown) => void;
sendActionRef: MutableRefObject<SendChatAction>;
prepareEditResend?: (
conversationId: string,
messageRef: HistoryMessageRef,
options?: { activate?: boolean },
) => Promise<{ conversationId: string; messageRef: HistoryMessageRef }>;
};

export function useEditResend(params: UseEditResendParams) {
Expand All @@ -27,7 +22,6 @@ export function useEditResend(params: UseEditResendParams) {
currentConversationIdRef,
onError,
sendActionRef,
prepareEditResend,
} = params;
const editResendInFlightRef = useRef(false);

Expand All @@ -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) {
Expand All @@ -76,7 +69,6 @@ export function useEditResend(params: UseEditResendParams) {
isConversationHydrating,
isSending,
onError,
prepareEditResend,
sendActionRef,
],
);
Expand Down
37 changes: 1 addition & 36 deletions crates/agent-gui/test/chat/edit-resend-atomic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 0 additions & 2 deletions crates/agent-ui/src/i18n/translations/enUSCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions crates/agent-ui/src/i18n/translations/zhCNCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "保存",
Expand Down
Loading