Skip to content

fix(chat): point floating window at the targeted chat - #1160

Open
yoruuuchan wants to merge 1 commit into
AAswordman:devfrom
yoruuuchan:fix/workflow-floating-chat-target
Open

fix(chat): point floating window at the targeted chat#1160
yoruuuchan wants to merge 1 commit into
AAswordman:devfrom
yoruuuchan:fix/workflow-floating-chat-target

Conversation

@yoruuuchan

Copy link
Copy Markdown
Contributor

变更说明 / Description

工作流触发 chat_with_agent 时,弹出的悬浮窗显示的是用户当前所在的对话,而不是 chat_id 指定的目标对话。本 PR 让悬浮窗在定向发送时跟随目标对话。

背景与动机 / Context and motivation

chat_with_agent(内置包 extended_chat,工作流节点 extended_chat:chat_with_agent 走的是同一份 JS)分两步工作:

  1. Tools.Chat.startService()start_chat_service,构造 FloatingChatService 的 Intent 并 setFloatingWindowVisible(true)。这个 Intent 只携带 INITIAL_MODE / AUTO_ENTER_VOICE_CHAT / WAKE_LAUNCHED / AUTO_EXIT_AFTER_MS / KEEP_IF_EXISTS,没有任何 chat id。
  2. Tools.Chat.sendMessage(message, chatId, ...)send_message_to_ai,目标 chat id 只出现在这一步。

第 2 步通过 core.sendUserMessage(chatIdOverride = targetChatId) 把消息正确投递到目标对话,但悬浮窗渲染的是 chatCore.currentChatId。FLOATING runtime 是 ChatSelectionMode.LOCAL_ONLY,启动时快照全局 currentChatId,之后由 ChatRuntimeHolder.registerChatSelectionSync(MAIN -> FLOATING) 持续跟随主界面。因此目标 chat id 从未传到显示层,窗口停在前台对话 A 上,而消息实际发往 B。

send_message_to_ai 未指定 runtime 时默认走 FLOATING,所以 chat_with_agent 与 external chat 的 chat_id + show_floating 组合都是这个形状。

改动范围 / Changes

  • StandardChatManagerTool.startMessageToAIStream:定向发送且 runtime 解析为 FLOATING 时,先调用既有的 ChatServiceCore.switchChatLocal(targetChatId) 让悬浮窗跟随目标对话,再发送消息。switchChatLocal 只改 FLOATING runtime 的本地状态,不写回全局 currentChatId,主界面不受影响,符合 docs/doc-src/architecture/chat_runtime_foreground_service_plan.md 中「FLOATING Core 只响应自己的切换」的约定。
  • 新增 FloatingDisplayChatSelector:把「哪个 runtime 该跟随目标对话」的判定抽成纯函数,便于回归测试。MAIN runtime 明确返回 null,后台发送不会把用户正在读的对话顶掉;目标为空、为空白或与当前显示相同时也返回 null。
  • 顺手把 runtimeSlot ?: ChatRuntimeSlot.FLOATING 提为 effectiveSlot,让 FLOATING 这个默认值只表达一次。
  • 新增 FloatingDisplayChatSelectorTest,6 个用例。
  • docs/doc-src/package-dev/chat.md:在 sendMessageoptions 说明里补一条定向发送时的悬浮窗跟随行为。

修在 startMessageToAIStream 这个唯一汇聚点上,工作流、AI 直接调用 send_message_to_ai、以及 external intent / HTTP / A2A 的定向流式发送一并覆盖,不需要改 extended_chat 的 TS 源与产物,因此不涉及 sync_example_packages.py 重新生成。

明确不包含:#555 的第 2 项「后台静默执行 / 完全不弹悬浮窗」。那是独立的 feature,本 PR 不做,所以合并后 #555 不会自动关闭。

兼容性与风险 / Compatibility and risks

  • 无 API、数据格式、配置变更;send_message_to_ai 的参数 schema 不变,AI 侧提示词无需改动。
  • 行为变化:带 chat_id 且走 FLOATING runtime 的发送,现在会让悬浮窗切到该对话。这正是 issue 期望的行为。走 MAIN runtime 的发送行为不变。
  • switchChatLocal 是 fire-and-forget,与 ChatRuntimeHolder.syncChatSelection 和悬浮窗历史选择器的既有用法一致,未额外加等待。切换期间 ChatHistoryDelegate.addMessageToChat 仍会持久化消息,不会丢消息。

关联 Issue / Related issue

Part of #555

只修复第 1 项「悬浮窗指向错误的对话」。第 2 项「静默后台执行」尚未实现,因此这里刻意不使用 Closes / Fixes / Resolves,避免合并后自动关闭整个 Issue。

验证方式 / Verification

检查或命令:./gradlew :app:compileDebugKotlin :app:testDebugUnitTest --tests "com.ai.assistance.operit.core.tools.defaultTool.standard.FloatingDisplayChatSelectorTest"
环境与变体:Windows 11,仓库自带 gradlew,JDK 21,debug 变体
结果:BUILD SUCCESSFUL in 31s;测试报告 tests=6 failures=0 errors=0 skipped=0

需要说明的两点:

  1. 在干净的 dev 上,:app:testDebugUnitTest 会在 :app:compileDebugUnitTestKotlin 阶段失败,原因是两个既有测试文件已经过期,与本 PR 无关:XaiProviderReasoningTest.kt 引用了 app/src/main 中已不存在的 XaiReasoningMapperxaiModelSupportsReasoningEffortDeepseekProviderMediaRoleTest.kt:244 直接调用了 OpenAIResponsesProvider 中为 protectedcreateRequestBody。为了跑通本次新增测试,我在本地临时把这两个文件移出源码树,跑完后原样还原,它们不在本 PR 的 diff 中。是否需要单独修,请维护者定夺。
  2. 未做真机验证。复现该场景需要在设备上建两个对话、配一个定时工作流节点并消耗真实 API 额度,而当前连接的设备上装的是日常在用的 Operit,安装 debug 包有覆盖用户数据的风险,因此按用户要求跳过。:app:compileDebugKotlin 通过,改动为纯逻辑分支且有单测覆盖判定函数。

android-tests.ymlandroid-build.yml 只在 push 到 main 或手动触发时运行,pr-check.ymlpull_request 目标是 development,所以本 PR 面向 dev 不会有自动检查,上面的本地结果是唯一验证依据。

证据 / Evidence

根因链路的关键代码位置,便于 review 时对照:

  • app/src/main/assets/packages/extended_chat.js:377:420startService()sendMessage(..., chatId, ...) 的调用顺序
  • core/tools/defaultTool/standard/StandardChatManagerTool.ktstartChatService,Intent extras 中无 chat id
  • api/chat/ChatRuntimeHolder.ktregisterChatSelectionSync(MAIN -> FLOATING)
  • services/core/ChatHistoryDelegate.ktChatSelectionMode.LOCAL_ONLY 初始化分支
  • ui/floating/ui/window/screen/FloatingChatWindowScreen.kt,悬浮窗按 chatCore.currentChatId 渲染

检查清单 / Checklist

  • 我已记录可复现验证和未运行项原因 / Reproducible verification and reasons for unrun checks are recorded
  • 日常开发 PR 的目标分支为 dev;如目标为 main,我已说明这是维护者发布同步 / The target branch is dev for regular work; if it is main, I explained why this is a maintainer-led release sync
  • 我已确认 Candidate checks 覆盖改动范围,并会处理技术失败项 / Candidate checks covers the change scope and technical failures will be addressed
  • 最终 diff 无无关、临时、生成、二进制或敏感内容 / Final diff has no unrelated, temporary, generated, binary, or secret content
  • 已提供对应的回归、UI、文档/字符串或兼容性证据 / Relevant regression, UI, docs/strings, or compatibility evidence is provided

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant