feat(desktop): add launch at login and start-in-tray options - #3575
feat(desktop): add launch at login and start-in-tray options#3575YellowMark-Z wants to merge 4 commits into
Conversation
Windows users who keep Cindy running as a background assistant had no supported way to start it quietly: `windowsCloseBehavior` only governs what happens after the user clicks the close button, and nothing controlled initial window visibility. Copying Cindy.exe into the Startup folder — the obvious workaround — crashes, because the Electron runtime files next to the executable are no longer found. Add two Windows-only switches to the App Behavior section: - Launch Cindy at login, backed by app.setLoginItemSettings() so Electron resolves the executable path itself. - Start in the tray at login, which keeps the main window hidden on a login-item start. The window is created with `show: false` already, so this skips the show() call outright and there is no visible flash. The login item registers an --opened-at-login argument and startup reads it back from argv. Electron's wasOpenedAtLogin is macOS-only, so argv is the one signal available on both platforms. Hiding requires the tray icon to exist: ensureWindowsTray() can fail, and skipping show() without a tray icon would leave the user no way to reach the app except Task Manager. The tray check is evaluated last and the window is shown normally whenever it fails. Refs makecindy#3568 Signed-off-by: jiayi Zhou <125216952+YellowMark-Z@users.noreply.github.com>
|
| Filename | Overview |
|---|---|
| apps/desktop/src/main/launchAtLogin.ts | 新增 Windows 登录项管理与隐藏启动判定,但读取状态时未使用注册条目的自定义参数,无法正确识别已启用状态。 |
| apps/desktop/src/main/bootstrap-electron.ts | 接入隐藏启动判定及三个 IPC handler,托盘创建失败时会保留正常显示窗口的回退路径。 |
| apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx | 新增两个 Windows 设置开关及乐观更新,但其显示状态会受到主进程错误登录项查询结果影响。 |
| apps/desktop/src/preload/preload.ts | 为主窗口 renderer 暴露固定且类型化的登录启动设置接口。 |
| apps/desktop/src/main/window-behavior-settings-store.ts | 新增 startInTrayOnLogin 持久化字段,并对旧配置和非法值安全回退为 false。 |
| apps/desktop/src/shared/windowBehavior.ts | 新增跨层状态类型和 IPC 通道常量,接口形状与使用方保持一致。 |
Sequence Diagram
sequenceDiagram
participant U as 用户
participant R as Renderer 设置页
participant P as Preload
participant M as Electron Main
participant W as Windows 登录项
U->>R: 开启开机自启动
R->>P: setLaunchAtLogin(true)
P->>M: IPC invoke
M->>W: 注册 Cindy.exe --opened-at-login
M->>W: 无参数查询 openAtLogin
W-->>M: false(未匹配自定义参数条目)
M-->>R: false
R->>R: 开关回退为关闭
Prompt To Fix All With AI
### Issue 1
apps/desktop/src/main/launchAtLogin.ts:71
**登录项查询未匹配参数**
在 Windows 用户开启开机自启动或重新进入设置页时,登录项以 `--opened-at-login` 参数注册,但这里未传入相同参数便读取 `openAtLogin`,因此查询的是默认空参数登录项并返回 `false`,导致开关回退为关闭且「开机启动时收起到托盘」持续不可用。
```suggestion
return app.getLoginItemSettings({ args: [OPENED_AT_LOGIN_FLAG] }).openAtLogin;
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "feat(desktop): add launch at login and s..." | Re-trigger Greptile
| /** 读取系统登录项的当前状态。查询失败按「未启用」处理。 */ | ||
| export function readLaunchAtLogin(app: LoginItemApp): boolean { | ||
| try { | ||
| return app.getLoginItemSettings().openAtLogin; |
There was a problem hiding this comment.
在 Windows 用户开启开机自启动或重新进入设置页时,登录项以 --opened-at-login 参数注册,但这里未传入相同参数便读取 openAtLogin,因此查询的是默认空参数登录项并返回 false,导致开关回退为关闭且「开机启动时收起到托盘」持续不可用。
| return app.getLoginItemSettings().openAtLogin; | |
| return app.getLoginItemSettings({ args: [OPENED_AT_LOGIN_FLAG] }).openAtLogin; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/launchAtLogin.ts
Line: 71
Comment:
**登录项查询未匹配参数**
在 Windows 用户开启开机自启动或重新进入设置页时,登录项以 `--opened-at-login` 参数注册,但这里未传入相同参数便读取 `openAtLogin`,因此查询的是默认空参数登录项并返回 `false`,导致开关回退为关闭且「开机启动时收起到托盘」持续不可用。
```suggestion
return app.getLoginItemSettings({ args: [OPENED_AT_LOGIN_FLAG] }).openAtLogin;
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Confirmed and fixed in d845cd8 — thanks, this was a real defect.
Electron's own typings spell out the semantics: for getLoginItemSettings(options), args is "the command-line arguments to compare against" and "defaults to an empty array" (win32). Since the entry is registered with --opened-at-login, querying without it compared against an empty command line and never matched, so openAtLogin came back false unconditionally. The switch flipped back to off as soon as the settings section mounted, and "Start in the tray at login" stayed disabled because it gates on that value — the feature was unreachable.
readLaunchAtLogin() now passes the same args, and LoginItemApp accepts the options argument.
One note on why the existing tests missed it: the old double ignored the arguments it was handed, so the defect passed. It now keys entries by args the way Windows does, which means a query that omits them genuinely fails to match. I verified the new tests actually catch the regression by reverting the one-line fix and re-running — 4 cases failed, including a round-trip case that enables the item and reads it straight back (the exact sequence the settings section performs on mount).
Verification:
pnpm --filter desktop exec vitest run --pool=forks \
src/main/__tests__/launchAtLogin.test.ts \
src/main/__tests__/windowBehaviorSettingsStore.test.ts
→ 26 passed (26), was 23
pnpm --filter desktop run --if-present typecheck → EXIT 0
pnpm test:unit:related → PASS, EXIT 0
pnpm exec eslint <both changed files> → EXIT 0
pnpm check:dco → 2 commits signed off
WindowBehaviorSection.tsx is unchanged: it was flagged as affected, but the root cause was entirely in the main-process query, and the renderer reads whatever main reports.
Still not verified, same as in the PR description: no manual UI pass in either theme, and the end-to-end login-item path (register → reboot Windows → confirm the window stays hidden with the tray icon present) has not been exercised.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc76e70f91
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| /** 读取系统登录项的当前状态。查询失败按「未启用」处理。 */ | ||
| export function readLaunchAtLogin(app: LoginItemApp): boolean { | ||
| try { | ||
| return app.getLoginItemSettings().openAtLogin; |
There was a problem hiding this comment.
Query the login item with its registered arguments
On Windows, Electron matches login items using the executable path and argument list, and getLoginItemSettings() defaults to an empty argument list. Since the item is registered with [OPENED_AT_LOGIN_FLAG] but queried without options here, enabling it can successfully create the startup entry while this read immediately reports false; the UI then turns the first switch back off and leaves the dependent start-in-tray switch disabled. Pass the same arguments when querying that are used by setLoginItemSettings().
Useful? React with 👍 / 👎.
| startInTrayOnLogin: readWindowBehaviorSettings().startInTrayOnLogin, | ||
| }), | ||
| ); | ||
| ipcMain.handle(WINDOW_BEHAVIOR_SET_LAUNCH_AT_LOGIN_CHANNEL, async (_e, enabled: unknown) => { |
There was a problem hiding this comment.
Validate the sender before changing login settings
When either new setter is invoked from anything other than a trusted Cindy top-level renderer, the handlers still mutate an OS login item or persistent startup preference because they ignore the IPC event and never call assertTrustedAppRendererEvent. These are new privileged IPC capabilities, so validate the sender before performing either side effect rather than relying on the renderer UI to limit access.
AGENTS.md reference: AGENTS.md:L30-L31
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid — fixed in 6ef0746.
All three handlers now call assertTrustedAppRendererEvent(event) before anything else, matching the surrounding privileged handlers in this file. Writing an OS login item is not something to gate on the renderer UI.
I left the two pre-existing windowBehavior setters beside them untouched, on the reading that electron-security-and-process-boundaries.md does not call for a sweep of existing handlers ("存量 IPC 数量较大,尚未全部迁入统一 sender guard。本规则不触发存量 handler 专项整改") while explicitly refusing that as an excuse for new ones ("新增 handler 不得以'旧代码没校验'为理由省略 sender 与 payload 验证"). Happy to widen the change if you'd rather have them done here.
Also worth noting I read the wrong rule file when first writing this — configuration-and-overrides.md rather than the Electron security one, which is what AGENTS.md L30-31 points at for IPC work. That's on me.
On Windows the `args` option of getLoginItemSettings() is the command line to compare against, and it defaults to an empty array. The entry is registered with `--opened-at-login`, so querying without that argument compared against an empty command line, never matched our entry, and always reported openAtLogin as false. The switch therefore reverted to off as soon as the settings section mounted, which also left "Start in the tray at login" permanently disabled — the whole feature was unreachable. Pass the same args when reading. The test double now keys entries by args the way Windows does, so a query that omits them no longer matches; the previous double ignored its arguments and let the defect pass. Signed-off-by: jiayi Zhou <125216952+YellowMark-Z@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d845cd8b10
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| */ | ||
| export function readLaunchAtLogin(app: LoginItemApp): boolean { | ||
| try { | ||
| return app.getLoginItemSettings({ args: [OPENED_AT_LOGIN_FLAG] }).openAtLogin; |
There was a problem hiding this comment.
There was a problem hiding this comment.
Valid — fixed in f0782c4, though not as a straight swap.
The two fields answer different halves of the question and neither is sufficient alone:
openAtLoginhonoursargs, so it tells us whether the entry is the one we registered.executableWillLaunchAtLoginreports whether the run key is still active, but per Electron's docs it "ignores theargsoption, this property will be true if the given executable would be launched at login with any arguments" — so on its own it cannot distinguish our entry from any other.
Replacing the first with the second would have traded this bug for a different one (and undone d845cd8, which exists precisely because args matching matters here). So the read now requires both.
executableWillLaunchAtLogin is Windows-only, so a missing value is deliberately not treated as deactivated — otherwise the read would break wherever the field is absent. There's a case covering that.
The test double now models the run key separately from the entry, the way Windows does. I verified the new coverage actually catches the defect by reverting the fix to read only openAtLogin and re-running: exactly the deactivated-run-key case failed, the other 19 passed.
Verification:
pnpm --filter desktop exec vitest run --pool=forks \
src/main/__tests__/launchAtLogin.test.ts \
src/main/__tests__/windowBehaviorSettingsStore.test.ts
→ 28 passed (28), was 26
pnpm --filter desktop run --if-present typecheck → EXIT 0
pnpm test:unit:related → PASS, EXIT 0
pnpm check:dco → 4 commits signed off
On lint: eslint on the three changed files reports 6 unused-import errors in bootstrap-electron.ts. I checked those against the base commit (b7837c74) and they reproduce there identically — pre-existing, not introduced here, so left alone.
Not verified: the end-to-end path still hasn't been exercised on a real reboot, and I haven't reproduced the Task Manager deactivation on a live machine — that branch is covered by the unit test only.
The three new channels write an OS login item and a persistent startup preference, so they are privileged capabilities and must not rely on the renderer UI to limit who can reach them. They ignored the IPC event and never checked where the call came from. Call assertTrustedAppRendererEvent() first in each handler, matching how the other privileged handlers in this file are written. The two pre-existing windowBehavior setters next to these are left alone: electron-security-and-process-boundaries.md does not ask for a sweep of existing handlers, but it does say new ones may not skip sender validation because older code lacks it. Signed-off-by: jiayi Zhou <125216952+YellowMark-Z@users.noreply.github.com>
|
@YellowMark-Z 👋 这个 PR 还有 4 条 review conversation 没 resolve(apps/desktop/src/main/launchAtLogin.ts / apps/desktop/src/main/bootstrap-electron.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
Disabling Cindy under "Startup apps" in Task Manager deactivates the run key but leaves the registry entry in place, so `openAtLogin` stays true. The switch therefore showed as on while nothing would actually launch, it kept "Start in the tray at login" enabled, and toggling the switch on again could not restore autostart because it already read as on. Read both fields and require both. They answer different halves of the question: `openAtLogin` honours `args`, so it tells us whether the entry is the one we registered; `executableWillLaunchAtLogin` reports whether the run key is still active but ignores `args`, so on its own it cannot tell our entry apart from any other. Neither alone is sufficient. `executableWillLaunchAtLogin` is Windows-only, so a missing value is not treated as deactivated — otherwise the read would break on platforms that never report it. The test double now models the run key separately from the entry, the way Windows does, and covers both the deactivated case and the platform that omits the field. Signed-off-by: jiayi Zhou <125216952+YellowMark-Z@users.noreply.github.com>
|
这条 PR 被维护者确认门拦住了(产品:开机启动/托盘启动设置;同时碰到 preload 安全边界)。请等维护者在本 PR 上 Approve;需要修改就 Request Changes。讨论 issue:#3576 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0782c45a9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| // 回传写入后重新查询到的事实状态:用户没权限改登录项时 renderer 要把 | ||
| // 乐观更新退回真实值,而不是显示一个并未生效的开启态。 | ||
| return writeLaunchAtLogin(app, enabled); |
There was a problem hiding this comment.
即便已经校验 sender,这个新 handler 仍可被任意 Cindy 顶层 renderer 调用,而 preload 在所有平台都暴露了该方法;因此在 macOS 上,遭遇 XSS 的 renderer 可以绕过仅存在于 UI 的 isWindows 判断,调用 Electron 同样支持的 setLoginItemSettings() 建立持久化登录项。请在 main 的写入 handler 中显式校验 process.platform === 'win32',不要把隐藏设置项当成权限边界。
AGENTS.md reference: AGENTS.md:L30-L31
Useful? React with 👍 / 👎.
| const startHidden = shouldStartHiddenInTray({ | ||
| platform: process.platform, | ||
| argv: process.argv, | ||
| startInTrayOnLogin: readWindowBehaviorSettings().startInTrayOnLogin, |
There was a problem hiding this comment.
| if (typeof enabled !== 'boolean') { | ||
| throwIpcError('INVALID_PARAMS', 'startInTrayOnLogin required (boolean)'); | ||
| } | ||
| writeStartInTrayOnLogin(enabled); |
There was a problem hiding this comment.
当设置文件只读、磁盘已满或系统登录项 API 抛错时,这里和上面的 writeLaunchAtLogin() 会让底层异常直接穿过新 IPC handler;文件写入错误通常包含用户数据目录等内部绝对路径,而且不符合 renderer 依赖的 [IpcErrorCode] 协议。请捕获两个写入调用并通过 throwIpcError('INTERNAL', ...) 返回受控错误。
AGENTS.md reference: AGENTS.md:L103-L105
Useful? React with 👍 / 👎.
|
@YellowMark-Z 👋 这个 PR 还有 7 条 review conversation 没 resolve(apps/desktop/src/main/launchAtLogin.ts / apps/desktop/src/main/bootstrap-electron.ts),auto-review 因此暂时跳过、没法继续审查 / 合并。 如果你已经按评论改完或回应了,请到对应 thread 上点 Resolve conversation;全部 resolve 后,下一轮 auto-review 会自动重新审查这个 PR。 |
|
命中 UI 路径(apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范 |
这次改了什么
摘要
把 Cindy 设为开机自启动后,主窗口每次登录都会弹出来。对于把 Cindy 当常驻后台助手、整天挂着的用户,这个窗口是每次登录都要手动关掉的干扰——期望的是进程在跑、托盘图标在,但不弹窗。
在此之前没有受支持的做法:
windowsCloseBehavior只接受quit/tray,管的是用户点关闭按钮之后的行为,不是启动时窗口如何呈现;代码里也没有控制初始窗口可见性的设置项或命令行开关。用户能想到的绕行办法——把Cindy.exe复制进启动目录——会直接崩溃,因为可执行文件旁边的 Electron 运行时文件找不到了:本 PR 在「应用行为」里加两个 Windows-only 开关:
app.setLoginItemSettings(),由 Electron 自己解析可执行文件路径与工作目录,顺带避开上面那个复制 exe 的坑。show: false创建,所以这里是直接不调show(),不存在"先显示再隐藏"的闪现。两个设计要点:
用 argv 标记判定自启动场景。
app.getLoginItemSettings().wasOpenedAtLogin只在 macOS 有值,Windows 恒为 false。所以注册登录项时追加--opened-at-login,启动时从process.argv读回——这条路径两个平台同构。手动双击图标时 argv 里没有它,窗口照常显示。托盘创建失败必须回退到显示窗口。
ensureWindowsTray()会因图标资源缺失等原因返回 false;那时若仍跳过show(),用户既没有窗口也没有托盘图标,只剩任务管理器可用。因此托盘检查放在四个条件的最后求值(前三条不满足时不产生建托盘的副作用),失败即正常显示窗口。这条边界有专门的单测覆盖。变更类型
feat新功能fix缺陷修复refactor/perf重构或性能优化docs/test/chore文档、测试或工程维护范围
main/launchAtLogin.ts(新增):登录项读写 +shouldStartHiddenInTray()判定,逻辑以纯函数形式暴露便于单测window-behavior-settings-store.ts:新增startInTrayOnLogin字段,默认falsebootstrap-electron.ts:ready-to-show分支接入启动隐藏;3 个新 IPC handlershared/windowBehavior.ts:新增 channel 常量与LaunchAtLoginStatepreload.ts/vite-env.d.ts:暴露 API 与类型声明WindowBehaviorSection.tsx:两张开关卡片;BehaviorCard增加disabled支持launchAtLogin.test.ts(新增,15 例)+windowBehaviorSettingsStore.test.ts(+5 例)windowsCloseBehavior的任何行为。UI 变化
未附截图:设置页已在 dev 实例中人工目检(Light / Dark 两种模式,见「手工验证」),但当前环境无法导出截图文件,故以文字如实描述核对结果,不以代码推断代替视觉证据。
BehaviorCard组件与其 token(--settings-theme-card-bg、--settings-theme-card-border、--settings-section-sublabel),因此双模式覆盖是继承自既有组件的。Switch组件自身已有--switch-disabled-opacity/--switch-disabled-thumb-opacity两个专用 token 表达不可用,因此卡片层只对文字区加opacity-60,不在外层叠整体透明度——叠加会把 Switch 压成两级灰、失去组件既定的禁用语义。怎么验证的
自动验证
新增测试覆盖的关键分支:
ensureTray返回 false)时回退到显示窗口ensureTray(不产生副作用)args,避免留下 Electron 匹配不到的孤儿登录项手工验证
Windows 11。
pnpm restart:desktop:remote --region=global启动隔离沙箱 dev 实例:在设置 →「应用行为」核对(含修复后的
d845cd8):登录项写入也拿到了实证:开启「开机时启动 Cindy」后
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run出现确认
setLoginItemSettings()确实注册了登录项,且--opened-at-login参数按设计带上(dev 实例指向仓库内的 electron,属预期)。该条目在验证后已手动清除。未执行的验证
登录项端到端行为未验证:已确认登录项能正确写入注册表(见「手工验证」),但完整路径——重启 Windows → 确认主窗口保持隐藏且托盘图标在 → 点击托盘图标唤出窗口——需要真实重启,未执行。托盘创建失败的回退分支仅有单测覆盖,未在真机上制造过该故障。
pnpm --filter desktop lint(全量)未通过:报 447 个问题(442 error / 5 warning),全部位于本 PR 未触碰的文件(shareConversationImage.ts、builtinPanels.tsx、agentInputQueue.ts、theme-import/color.ts等),属仓库既有状态。已单独对本 PR 改动的 7 个文件跑 eslint,零问题(见上)。既有问题不在本 PR 范围内,未一并修复。macOS / Linux 未验证:功能按平台判定,
shouldStartHiddenInTray()在非 win32 直接返回 false,UI 也不渲染;有单测覆盖 darwin / linux 分支。风险
风险分类
影响与回滚
影响范围
process.platform === 'win32'生效,macOS / Linux 上shouldStartHiddenInTray()提前返回 false、UI 不渲染。既有平台行为不变。app.setLoginItemSettings()在 Windows 上写HKCU\...\CurrentVersion\Run(当前用户,不需要管理员权限),只在用户主动打开开关时调用。用户也可在任务管理器「启动应用」里禁用;因此 UI 每次挂载都重新向系统查询真实状态,不缓存、不持久化"是否已启用",避免与系统状态漂移。window-behavior-settings.json只新增startInTrayOnLogin一个布尔字段;normalize()对缺失或非布尔值回落到false,旧配置文件可直接读取,无需迁移。startInTrayOnLogin与launchAtLogin互相独立。关闭自启动不清除startInTrayOnLogin,用户重新打开自启动时保留原选择;代价是设置文件里可能存在"自启动已关但 startInTrayOnLogin 为 true"的组合,该组合无副作用(判定的第二个条件不满足)。回滚 / 降级方式
startInTrayOnLogin字段会被normalize()忽略(未知字段不影响读取)。提交前检查
git commit -s,见 DCO)