bug: Windows 上主窗口被后台插件 focus 请求抢占焦点(盖住当前前台应用)
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 问题描述 / What happened
Windows 上,当用户正在前台使用**其它应用**(反馈者录屏中为 QQ)时,Cindy 主窗口会**在没有任何用户操作**的情况下突然被拉到前台、抢走焦点,盖住用户当前正在用的窗口。
- 实际行为:主窗口无用户手势自动 `show()+focus()`,强行前台化。
- 期望行为:后台/程序化触发的"聚焦某会话"不应打断用户当前前台应用;至多用任务栏闪烁提醒。
## 环境 / Environment
- Cindy 版本或 commit:待反馈者补充(基于当前 `main` 分析)
- 平台与版本:Windows(具体版本待补充);macOS 未复现(见下方原因)
- 安装方式:桌面客户端
## 复现步骤 / Steps to reproduce
(根因分析指向的路径,待反馈者确认当时是否有插件在跑)
1. Windows 上运行 Cindy,主窗口非前台(用户切到 QQ 等其它应用)。
2. Cindy 内有 Ghost 插件在后台 agent turn / 工具调用上下文中运行。
3. 该插件发出 `workspace({ focus: true })` 请求。
4. 主窗口被 `show()+focus()` 拉到前台,盖住 QQ。
## 根因分析 / Root cause
对 `apps/desktop/src/main/` 做了穷举审计,Windows 上**唯一**"无用户手势就 `show()+focus()` 主窗口"的链路:
```
Ghost 插件后台发 workspace({ focus: true })
→ cindy-brain/workspaceSlot.ts:254 / 259 service.focusSession(sessionId)
→ maker-ipc/register.ts:4995 focusSession = openMainWindowSession
→ deepLink.ts:317 openMainWindowSession
→ deepLink.ts dispatchDeepLink({type:'session'})
→ deepLink.ts:353-356 win.show(); win.restore(); win.focus() // 裸抢前台
```
`dispatchDeepLink` 的设计前提是"所有 deep link 都是用户点 `cindy://` 链接,用户就是想看到 app"(见该函数注释),因此**无条件**前台化、**无平台判断、无用户手势判据、无抢焦点抑制**。但经 `openMainWindowSession` 注入给 Ghost workspace slot 的这条 `focusSession` **不是用户点击**——插件在后台上下文运行,用户可能正在别的应用前台。
**为什么只在 Windows 明显**:`BrowserWindow.show()` 在 Windows 上会同时置前+抢焦点,`win.focus()` 强制前台化;macOS 对后台应用抢前台有系统级限制(通常只弹 Dock),同段代码在 Mac 上不表现为"覆盖当前应用"。
### 已排除(审计覆盖)
- Agent Island 整条链路 macOS-only(`shared/agentIsland.ts:195` 非 `darwin` 直接 return false),Windows 不触发。
- 通知(agent turn 完成 / 定时任务终态):只弹 toast,前台化严格 gate 在"点通知"之后;turn 完成本身走 `flashFrame`(任务栏闪烁,不抢焦点)。
- device-link 远程控制、hook-server 入站、scheduler、orca、IM(飞书 bot):主进程内零主窗口前台化调用。
- Ghost 面板 / 右侧栏 / 会话副窗口 / 语音浮层 / 权限引导浮层:操作独立窗口,不会让主窗口盖住 QQ。
## 建议修复方向 / Suggested fix
区分"用户手势 deep link"与"程序化(Ghost)focus":给 `openMainWindowSession` / `dispatchDeepLink` 传 `userGesture: boolean`,Ghost 注入路径传 `false`。非用户手势时:
- 应用已在前台 → 正常切会话 `show()+focus()`;
- 应用不在前台 → **不 `focus()`**,改用任务栏提醒(`flashFrame(true)` + `setOverlayIcon`,与 `appBadgeService` 现有非抢焦点做法一致),只做后台导航。
真实 `cindy://` 用户点击(open-url / second-instance / OAuth 返回)保持 `userGesture: true`,行为不变。
## 日志与截图 / Logs & screenshots
反馈者提供了录屏(QQ 前台被 Cindy 主窗口覆盖)。如能提供复现时刻 `apps/desktop/logs/` 中的日志(搜 `deep-link` / `buffered pending` / `openMainWindowSession`),可进一步确认是否为该 Ghost 路径。
> 待确认:反馈者复现时是否安装/运行了 Ghost 插件。若完全未使用任何插件,说明存在审计未覆盖的触发点,需据录屏时间点 + 日志再排查。
Contributor guide
Research direction
Start by reading cindy-brain/workspaceSlot.ts around lines 254/259, maker-ipc/register.ts around line 4995, and deepLink.ts around lines 317 and 353-356. Check apps/desktop/logs/ for deep-link, buffered pending, and openMainWindowSession entries, then reproduce the Ghost workspace focus path on Windows. Done means background focus requests do not cover another foreground application, while genuine cindy:// user actions retain their current behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, typescript
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 64/100