From a90fda96a77ad952264a1fb1ccdbd3dbb7a228a1 Mon Sep 17 00:00:00 2001 From: yoruuuchan <1587761204@qq.com> Date: Thu, 10 Sep 2026 18:16:07 +0900 Subject: [PATCH] fix(chat): point floating window at the targeted chat chat_with_agent opens the floating window through start_chat_service, which carries no chat id, and only afterwards routes the message with chat_id. The FLOATING runtime's currentChatId is kept in sync with the foreground chat, so the window stayed on the conversation the user was looking at while the message went to the target one. Follow the target chat with switchChatLocal on a targeted send. It only touches the floating runtime's local state and never writes back to the global current chat, so the main UI is unaffected. The MAIN runtime still does not switch, keeping a background send from yanking the conversation the user is reading. --- .../standard/StandardChatManagerTool.kt | 36 ++++++- .../FloatingDisplayChatSelectorTest.kt | 96 +++++++++++++++++++ docs/doc-src/package-dev/chat.md | 1 + 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/com/ai/assistance/operit/core/tools/defaultTool/standard/FloatingDisplayChatSelectorTest.kt diff --git a/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardChatManagerTool.kt b/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardChatManagerTool.kt index 9b5a01a745..26ee644999 100644 --- a/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardChatManagerTool.kt +++ b/app/src/main/java/com/ai/assistance/operit/core/tools/defaultTool/standard/StandardChatManagerTool.kt @@ -88,6 +88,26 @@ sealed class MessageSendStreamStartResult { data class Failed(val result: ToolResult) : MessageSendStreamStartResult() } +/** + * 定向发送时,决定悬浮窗应跟随哪个对话显示。 + * + * FLOATING runtime 的 currentChatId 由 ChatRuntimeHolder 从主界面同步而来,只用 chatIdOverride + * 发送会让消息进到目标对话、窗口却停在前台对话上。MAIN runtime 不做切换,否则后台发送会把用户 + * 正在看的对话顶掉。 + */ +internal object FloatingDisplayChatSelector { + fun select( + runtimeSlot: ChatRuntimeSlot, + targetChatId: String?, + displayedChatId: String? + ): String? { + if (runtimeSlot != ChatRuntimeSlot.FLOATING) return null + if (targetChatId.isNullOrBlank()) return null + if (targetChatId == displayedChatId) return null + return targetChatId + } +} + /** * 对话管理工具 * 负责管理对话、浮窗服务,以及按指定 runtime 发送消息 @@ -1733,7 +1753,8 @@ class StandardChatManagerTool(private val context: Context) { ) } - val core = chatRuntimeHolder.getCore(runtimeSlot ?: ChatRuntimeSlot.FLOATING) + val effectiveSlot = runtimeSlot ?: ChatRuntimeSlot.FLOATING + val core = chatRuntimeHolder.getCore(effectiveSlot) val message = tool.parameters.find { it.name == "message" }?.value if (message.isNullOrBlank()) { @@ -1892,7 +1913,18 @@ class StandardChatManagerTool(private val context: Context) { } if (hasTargetChat) { - // 后台发送到指定对话,不切换 UI + // 悬浮窗跟随目标对话:chatIdOverride 只路由消息,FLOATING runtime 的 + // currentChatId 仍停在前台对话上,窗口会显示错误的会话(#555)。 + // switchChatLocal 只改悬浮窗本地状态,不写回全局,主界面不受影响。 + FloatingDisplayChatSelector.select( + runtimeSlot = effectiveSlot, + targetChatId = targetChatId, + displayedChatId = core.currentChatId.value + )?.let { chatToFollow -> + core.switchChatLocal(chatToFollow) + } + + // 后台发送到指定对话,不切换主界面 core.sendUserMessage( promptFunctionType = PromptFunctionType.CHAT, roleCardIdOverride = roleCardId, diff --git a/app/src/test/java/com/ai/assistance/operit/core/tools/defaultTool/standard/FloatingDisplayChatSelectorTest.kt b/app/src/test/java/com/ai/assistance/operit/core/tools/defaultTool/standard/FloatingDisplayChatSelectorTest.kt new file mode 100644 index 0000000000..1c79311b8c --- /dev/null +++ b/app/src/test/java/com/ai/assistance/operit/core/tools/defaultTool/standard/FloatingDisplayChatSelectorTest.kt @@ -0,0 +1,96 @@ +package com.ai.assistance.operit.core.tools.defaultTool.standard + +import com.ai.assistance.operit.api.chat.ChatRuntimeSlot +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class FloatingDisplayChatSelectorTest { + + @Test + fun select_floatingRuntimeFollowsWorkflowTargetChatInsteadOfForegroundChat() { + // #555: a workflow sends to chat B while the user is looking at chat A. + val result = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.FLOATING, + targetChatId = "chat-b", + displayedChatId = "chat-a" + ) + + assertEquals("chat-b", result) + } + + @Test + fun select_floatingRuntimeFollowsTargetWhenNothingIsDisplayedYet() { + val result = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.FLOATING, + targetChatId = "chat-b", + displayedChatId = null + ) + + assertEquals("chat-b", result) + } + + @Test + fun select_mainRuntimeNeverSwitchesAwayFromForegroundChat() { + val result = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.MAIN, + targetChatId = "chat-b", + displayedChatId = "chat-a" + ) + + assertNull(result) + } + + @Test + fun select_returnsNullWhenTargetAlreadyDisplayed() { + val result = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.FLOATING, + targetChatId = "chat-a", + displayedChatId = "chat-a" + ) + + assertNull(result) + } + + @Test + fun select_returnsNullWhenNoTargetChatIsGiven() { + val floating = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.FLOATING, + targetChatId = null, + displayedChatId = "chat-a" + ) + val main = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.MAIN, + targetChatId = null, + displayedChatId = "chat-a" + ) + + assertNull(floating) + assertNull(main) + } + + @Test + fun select_returnsNullWhenTargetChatIsBlank() { + val blank = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.FLOATING, + targetChatId = " ", + displayedChatId = "chat-a" + ) + val empty = + FloatingDisplayChatSelector.select( + runtimeSlot = ChatRuntimeSlot.FLOATING, + targetChatId = "", + displayedChatId = "chat-a" + ) + + assertNull(blank) + assertNull(empty) + } +} diff --git a/docs/doc-src/package-dev/chat.md b/docs/doc-src/package-dev/chat.md index 6171a72880..2c45912505 100644 --- a/docs/doc-src/package-dev/chat.md +++ b/docs/doc-src/package-dev/chat.md @@ -103,6 +103,7 @@ findChat({ query, match?, index? }): Promise 其中: - `runtime` 用于指定本次消息发送到哪个 chat runtime,未指定时默认走 `floating` +- 传入 `chatId` 时消息只投递到该对话,走 `floating` 时悬浮窗会一并切到该对话显示,走 `main` 时不切换用户正在看的对话 - `timeout_ms` 用于控制本次发送的最长等待时间,单位毫秒 ### `call(options)`