🐛 fix: skip sendMessageInServer in Gateway mode + NavItem loading fix + i18n (#13681)

* 🐛 fix: reuse existing messages in execAgent when existingMessageIds provided

When existingMessageIds contains [userMsgId, assistantMsgId], skip
creating new messages and reuse the existing ones. This fixes duplicate
messages in Gateway mode where sendMessageInServer already created
the messages before execAgentTask is called.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: allow clicking NavItem while loading

Loading state should only show a visual indicator, not block onClick.
This fixes topic sidebar items being unclickable during agent execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "🐛 fix: reuse existing messages in execAgent when existingMessageIds provided"

This reverts commit 43b808024d5c4a0074b692a85083a72046ab47e0.

* 🐛 fix: skip sendMessageInServer in Gateway mode to avoid duplicate messages

Gateway mode now calls execAgentTask directly instead of going through
sendMessageInServer first. The backend creates user + assistant messages
and topic in one call. executeGatewayAgent handles topic switching
internally after receiving the server response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🌐 chore: add i18n for execServerAgentRuntime operation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: move temp message cleanup after executeGatewayAgent succeeds

Keep temp messages visible during the gateway call so the UI isn't
blank. On failure, mark the operation as failed instead of silently
returning — temp messages remain so the user sees something went wrong.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: remove manual temp message cleanup in gateway mode

switchTopic handles new topic navigation, and fetchAndReplaceMessages
replaces the message list from DB — no need to manually delete temp
messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: clear _new key temp messages when gateway creates new topic

Pass clearNewKey: true to switchTopic so temp messages from the
optimistic create don't persist in the _new key after switching
to the server-created topic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: import ExecAgentResult from @lobechat/types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu 2026-04-09 01:33:54 +08:00 committed by GitHub
parent e65e2c3628
commit 4d7cbfea8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 66 additions and 44 deletions

View file

@ -229,6 +229,7 @@
"operation.contextCompression": "Context too long, compressing history...",
"operation.execAgentRuntime": "Preparing response",
"operation.execClientTask": "Executing task",
"operation.execServerAgentRuntime": "Running… You can switch tasks or close the page—I'll keep going.",
"operation.sendMessage": "Sending message",
"owner": "Group owner",
"pageCopilot.title": "Page Agent",

View file

@ -229,6 +229,7 @@
"operation.contextCompression": "上下文过长,正在压缩历史记录……",
"operation.execAgentRuntime": "准备响应中",
"operation.execClientTask": "执行任务中",
"operation.execServerAgentRuntime": "运行中…你可以切换任务或关闭页面,我会继续执行。",
"operation.sendMessage": "消息发送中",
"owner": "群主",
"pageCopilot.title": "文稿助理",

View file

@ -107,7 +107,7 @@ const NavItem = memo<NavItemProps>(
if (href && !isModifierClick(e)) {
e.preventDefault();
}
if (disabled || loading) return;
if (disabled) return;
onClick?.(e);
}}
{...linkProps}

View file

@ -248,6 +248,8 @@ export default {
'operation.contextCompression': 'Context too long, compressing history...',
'operation.execAgentRuntime': 'Preparing response',
'operation.execClientTask': 'Executing task',
'operation.execServerAgentRuntime':
'Running… You can switch tasks or close the page — the task will keep going.',
'operation.sendMessage': 'Sending message',
'owner': 'Group owner',
'pageCopilot.title': 'Page Agent',

View file

@ -340,6 +340,31 @@ export class ConversationLifecycleActionImpl {
inputSendErrorMsg: undefined,
});
// ── Gateway mode: skip sendMessageInServer, let execAgentTask handle everything ──
if (this.#get().isGatewayModeEnabled()) {
this.#get().completeOperation(operationId);
try {
const result = await this.#get().executeGatewayAgent({
context: operationContext,
message,
});
return {
assistantMessageId: result.assistantMessageId,
userMessageId: result.userMessageId,
};
} catch (e) {
console.error('[Gateway] Failed to start server-side agent:', e);
this.#get().failOperation(operationId, {
message: e instanceof Error ? e.message : 'Unknown error',
type: 'GatewayError',
});
return;
}
}
// ── Client mode: send via server API then run agent locally ──
let data: SendMessageServerResponse | undefined;
try {
const { model, provider } = agentSelectors.getAgentConfigById(agentId)(getAgentStoreState());
@ -582,32 +607,7 @@ export class ConversationLifecycleActionImpl {
}
}
// ── AI execution ──
// Gateway mode: server-side execution via WebSocket (opt-in via Labs toggle)
if (this.#get().isGatewayModeEnabled()) {
try {
await this.#get().executeGatewayAgent({
assistantMessageId: data.assistantMessageId,
context: execContext,
message,
parentOperationId: operationId,
topicId: data.topicId,
userMessageId: data.userMessageId,
});
} catch (e) {
console.error('[Gateway] Failed to start server-side agent:', e);
if (data.topicId) this.#get().internal_updateTopicLoading(data.topicId, false);
}
return {
assistantMessageId: data.assistantMessageId,
createdThreadId: data.createdThreadId,
userMessageId: data.userMessageId,
};
}
// Client mode: run agent loop locally
// ── AI execution (client mode) ──
{
const displayMessages = displayMessageSelectors.getDisplayMessagesByKey(
messageMapKey(execContext),

View file

@ -1,4 +1,4 @@
import type { ConversationContext } from '@lobechat/types';
import type { ConversationContext, ExecAgentResult } from '@lobechat/types';
import type {
AgentStreamClientOptions,
@ -169,20 +169,27 @@ export class GatewayActionImpl {
* Execute agent task via Gateway WebSocket.
* Call isGatewayModeEnabled() first to check availability.
*/
/**
* Execute agent task via Gateway WebSocket.
* The backend creates user + assistant messages and the topic (if needed).
* Returns the result so the caller can handle topic switching.
*/
/**
* Execute agent task via Gateway WebSocket.
* The backend creates user + assistant messages and the topic (if needed),
* then starts the agent. This method handles topic switching and WebSocket connection.
*/
executeGatewayAgent = async (params: {
assistantMessageId: string;
context: ConversationContext;
message: string;
parentOperationId: string;
topicId?: string;
userMessageId: string;
}): Promise<void> => {
const { assistantMessageId, context, message, parentOperationId, topicId, userMessageId } =
params;
}): Promise<ExecAgentResult> => {
const { context, message } = params;
const agentGatewayUrl =
window.global_serverConfigStore!.getState().serverConfig.agentGatewayUrl!;
const isCreateNewTopic = !context.topicId;
const result = await aiAgentService.execAgentTask({
agentId: context.agentId,
appContext: {
@ -191,24 +198,33 @@ export class GatewayActionImpl {
threadId: context.threadId,
topicId: context.topicId,
},
existingMessageIds: [userMessageId, assistantMessageId],
prompt: message,
});
// If server created a new topic, switch to it and clean up the _new key temp messages
if (isCreateNewTopic && result.topicId) {
await this.#get().switchTopic(result.topicId, { clearNewKey: true });
}
// Use the server-created topicId for the execution context
const execContext = { ...context, topicId: result.topicId };
if (result.topicId) {
this.#get().internal_updateTopicLoading(result.topicId, true);
}
// Create a dedicated operation for gateway execution with correct context
const { operationId: gatewayOpId } = this.#get().startOperation({
context,
parentOperationId,
context: execContext,
type: 'execServerAgentRuntime',
});
// Associate the initial assistant message with the gateway operation
// so the UI shows loading/generating state via the operation system
this.#get().associateMessageWithOperation(assistantMessageId, gatewayOpId);
// Associate the server-created assistant message with the gateway operation
this.#get().associateMessageWithOperation(result.assistantMessageId, gatewayOpId);
const eventHandler = createGatewayEventHandler(this.#get, {
assistantMessageId,
context,
assistantMessageId: result.assistantMessageId,
context: execContext,
operationId: gatewayOpId,
});
@ -217,11 +233,13 @@ export class GatewayActionImpl {
onEvent: eventHandler,
onSessionComplete: () => {
this.#get().completeOperation(gatewayOpId);
if (topicId) this.#get().internal_updateTopicLoading(topicId, false);
if (result.topicId) this.#get().internal_updateTopicLoading(result.topicId, false);
},
operationId: result.operationId,
token: result.token || '',
});
return result;
};
private internal_cleanupGatewayConnection = (operationId: string): void => {