Skip to content

feat(desktop): add launch at login and start-in-tray options - #3575

Open
YellowMark-Z wants to merge 4 commits into
makecindy:mainfrom
YellowMark-Z:feat/start-in-tray
Open

feat(desktop): add launch at login and start-in-tray options#3575
YellowMark-Z wants to merge 4 commits into
makecindy:mainfrom
YellowMark-Z:feat/start-in-tray

Conversation

@YellowMark-Z

@YellowMark-Z YellowMark-Z commented Aug 28, 2026

Copy link
Copy Markdown

这次改了什么

摘要

把 Cindy 设为开机自启动后,主窗口每次登录都会弹出来。对于把 Cindy 当常驻后台助手、整天挂着的用户,这个窗口是每次登录都要手动关掉的干扰——期望的是进程在跑、托盘图标在,但不弹窗。

在此之前没有受支持的做法:windowsCloseBehavior 只接受 quit / tray,管的是用户点关闭按钮之后的行为,不是启动时窗口如何呈现;代码里也没有控制初始窗口可见性的设置项或命令行开关。用户能想到的绕行办法——把 Cindy.exe 复制进启动目录——会直接崩溃,因为可执行文件旁边的 Electron 运行时文件找不到了:

ERROR:base\i18n\icu_util.cc:232] Invalid file descriptor to ICU data received.

本 PR 在「应用行为」里加两个 Windows-only 开关:

  • 开机时启动 Cindy:走 app.setLoginItemSettings(),由 Electron 自己解析可执行文件路径与工作目录,顺带避开上面那个复制 exe 的坑。
  • 开机启动时收起到托盘:登录项拉起时保持主窗口隐藏。主窗口本来就以 show: false 创建,所以这里是直接不调 show(),不存在"先显示再隐藏"的闪现。

两个设计要点:

  1. 用 argv 标记判定自启动场景。 app.getLoginItemSettings().wasOpenedAtLogin 只在 macOS 有值,Windows 恒为 false。所以注册登录项时追加 --opened-at-login,启动时从 process.argv 读回——这条路径两个平台同构。手动双击图标时 argv 里没有它,窗口照常显示。

  2. 托盘创建失败必须回退到显示窗口。 ensureWindowsTray() 会因图标资源缺失等原因返回 false;那时若仍跳过 show(),用户既没有窗口也没有托盘图标,只剩任务管理器可用。因此托盘检查放在四个条件的最后求值(前三条不满足时不产生建托盘的副作用),失败即正常显示窗口。这条边界有专门的单测覆盖。

变更类型

  • feat 新功能
  • fix 缺陷修复
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:

范围

  • 关联 Issue / 需求:Add option to start minimized to system tray on launch #3568
  • 本 PR 包含:
    • main/launchAtLogin.ts(新增):登录项读写 + shouldStartHiddenInTray() 判定,逻辑以纯函数形式暴露便于单测
    • window-behavior-settings-store.ts:新增 startInTrayOnLogin 字段,默认 false
    • bootstrap-electron.tsready-to-show 分支接入启动隐藏;3 个新 IPC handler
    • shared/windowBehavior.ts:新增 channel 常量与 LaunchAtLoginState
    • preload.ts / vite-env.d.ts:暴露 API 与类型声明
    • WindowBehaviorSection.tsx:两张开关卡片;BehaviorCard 增加 disabled 支持
    • en / zh-CN / zh-TW / ja / ko 五份文案
    • launchAtLogin.test.ts(新增,15 例)+ windowBehaviorSettingsStore.test.ts(+5 例)
  • 明确不包含:
    • macOS / Linux 支持。托盘常驻语义与 macOS 的 Dock 不同,本次只做 Windows,UI 也只在 Windows 显示。
    • 修改既有 windowsCloseBehavior 的任何行为。
    • 仓库既有的 eslint 问题(见下文「未执行的验证」)。
  • 用户可见变化:Windows 设置 →「应用行为」新增两个开关,位于「保持电脑唤醒」之后、「关闭主窗口时」之前。默认全关,行为与当前版本完全一致。
  • 是否存在 breaking change:无

UI 变化

未附截图:设置页已在 dev 实例中人工目检(Light / Dark 两种模式,见「手工验证」),但当前环境无法导出截图文件,故以文字如实描述核对结果,不以代码推断代替视觉证据。

  • 引用的设计规范:
    • DESIGN.md §10「Light / Dark Dual-Mode Delivery Gate」:颜色全部通过语义 token 消费,无硬编码色值、无只适配单一模式的条件分支。新增的两张卡片直接复用同文件内既有的 BehaviorCard 组件与其 token(--settings-theme-card-bg--settings-theme-card-border--settings-section-sublabel),因此双模式覆盖是继承自既有组件的。
    • 同章「Both modes must cover every state actually touched by the change — ... disabled ...」:本次新引入了 disabled 状态Switch 组件自身已有 --switch-disabled-opacity / --switch-disabled-thumb-opacity 两个专用 token 表达不可用,因此卡片层只对文字区加 opacity-60不在外层叠整体透明度——叠加会把 Switch 压成两级灰、失去组件既定的禁用语义。
    • 按同章最后一条要求如实声明:token 复用本身不构成验证证据,故已另做实机目检——Light / Dark 两种模式均已在 dev 实例中确认,禁用态文字可读、与正常态可区分,无不可读或状态缺失。结论见「手工验证」。

怎么验证的

自动验证

pnpm --filter desktop run --if-present typecheck
结果:EXIT 0,零错误

pnpm test:unit:related
结果:RELATED related: apps/desktop (related 14)
      PASS apps/desktop unit (127.0s)
      EXIT 0

pnpm --filter desktop exec vitest run --pool=forks \
  src/main/__tests__/launchAtLogin.test.ts \
  src/main/__tests__/windowBehaviorSettingsStore.test.ts
结果:✓ launchAtLogin.test.ts (15 tests)
      ✓ windowBehaviorSettingsStore.test.ts (8 tests)
      Test Files  2 passed (2)
           Tests  23 passed (23)

pnpm exec eslint <本 PR 改动的 7 个 ts/tsx 文件>
结果:EXIT 0,零问题

pnpm check:dco
结果:DCO check passed: 1 commit signed off

新增测试覆盖的关键分支:

  • 四个条件的每一条单独不满足时都回退到显示窗口
  • 托盘创建失败(ensureTray 返回 false)时回退到显示窗口
  • 前置条件不满足时不调用 ensureTray(不产生副作用)
  • 关闭登录项时仍传 args,避免留下 Electron 匹配不到的孤儿登录项
  • 写入未生效(无权限)时回传真实状态而非请求值

手工验证

Windows 11。pnpm restart:desktop:remote --region=global 启动隔离沙箱 dev 实例:

DESKTOP_DEV_VERDICT=ready
mode=isolated
sandbox=dev
commit=d845cd8b1067ecaca7c9ba218baa3ff55c8718fe
region=global

在设置 →「应用行为」核对(含修复后的 d845cd8):

  1. 两个新开关按预期出现在「保持电脑唤醒」之后、「关闭主窗口时」之前,顺序正确。
  2. 「开机启动时收起到托盘」默认置灰不可点。
  3. 打开「开机时启动 Cindy」后,上述开关变为可点击,联动正常。
  4. Light / Dark 两种模式均已切换目检:禁用态的标题与说明文字清晰可读,与正常态可区分,未见糊成一片或状态缺失。

登录项写入也拿到了实证:开启「开机时启动 Cindy」后 HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run 出现

com.xd.cindy = "<repo>\node_modules\electron\dist\electron.exe" --opened-at-login

确认 setLoginItemSettings() 确实注册了登录项,且 --opened-at-login 参数按设计带上(dev 实例指向仓库内的 electron,属预期)。该条目在验证后已手动清除。

未执行的验证

  1. 登录项端到端行为未验证:已确认登录项能正确写入注册表(见「手工验证」),但完整路径——重启 Windows → 确认主窗口保持隐藏且托盘图标在 → 点击托盘图标唤出窗口——需要真实重启,未执行。托盘创建失败的回退分支仅有单测覆盖,未在真机上制造过该故障。

  2. pnpm --filter desktop lint(全量)未通过:报 447 个问题(442 error / 5 warning),全部位于本 PR 未触碰的文件(shareConversationImage.tsbuiltinPanels.tsxagentInputQueue.tstheme-import/color.ts 等),属仓库既有状态。已单独对本 PR 改动的 7 个文件跑 eslint,零问题(见上)。既有问题不在本 PR 范围内,未一并修复。

  3. macOS / Linux 未验证:功能按平台判定,shouldStartHiddenInTray() 在非 win32 直接返回 false,UI 也不渲染;有单测覆盖 darwin / linux 分支。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 存量插件兼容(批准状态 / 指纹 / manifest 校验 / 安装布局 / 包格式)
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:写入操作系统登录项

影响与回滚

影响范围

  • 跨平台差异:新增逻辑仅在 process.platform === 'win32' 生效,macOS / Linux 上 shouldStartHiddenInTray() 提前返回 false、UI 不渲染。既有平台行为不变。
  • 写入操作系统登录项:app.setLoginItemSettings() 在 Windows 上写 HKCU\...\CurrentVersion\Run(当前用户,不需要管理员权限),只在用户主动打开开关时调用。用户也可在任务管理器「启动应用」里禁用;因此 UI 每次挂载都重新向系统查询真实状态,不缓存、不持久化"是否已启用",避免与系统状态漂移。
  • 默认值:两项均默认关闭。未自定义的存量用户升级后行为与当前完全一致,不产生任何可见变化。
  • 配置持久化:window-behavior-settings.json 只新增 startInTrayOnLogin 一个布尔字段;normalize() 对缺失或非布尔值回落到 false,旧配置文件可直接读取,无需迁移。
  • 已知取舍:startInTrayOnLoginlaunchAtLogin 互相独立。关闭自启动不清除 startInTrayOnLogin,用户重新打开自启动时保留原选择;代价是设置文件里可能存在"自启动已关但 startInTrayOnLogin 为 true"的组合,该组合无副作用(判定的第二个条件不满足)。

回滚 / 降级方式

  • 整体回滚:revert 本 commit 即可,没有 migration、没有协议变更、没有需要清理的持久化状态。已写入的 startInTrayOnLogin 字段会被 normalize() 忽略(未知字段不影响读取)。
  • 用户侧降级:关掉「开机启动时收起到托盘」即恢复启动显示窗口;关掉「开机时启动 Cindy」即移除系统登录项。
  • 需要注意:revert 后已注册的系统登录项不会自动移除(它在注册表里,不随应用代码回滚)。受影响用户需在任务管理器「启动应用」中手动禁用,或在回滚前先关掉开关。

提交前检查

  • 已 review 完整 diff
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO
  • UI 改动已在「UI 变化」注明引用的设计规范章节(不涉及 UI 则跳过)
  • 未提交凭证、令牌或授权文件
  • 已补充必要文档(改动模块的头注释已同步更新;无独立文档需要新增)
  • 已确认测试结果或说明未执行原因

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>
@YellowMark-Z
YellowMark-Z requested a review from a team as a code owner August 28, 2026 11:18
@greptile-apps

greptile-apps Bot commented Aug 28, 2026

Copy link
Copy Markdown

Greptile Summary

此 PR 为 Windows 桌面端新增开机自启动和登录时隐藏到托盘的设置,并贯通主进程、preload、renderer 与本地偏好存储。

  • 使用自定义命令行标记区分登录项启动与手动启动
  • 启动隐藏前确保托盘创建成功,失败时回退到显示主窗口
  • 新增两个设置开关、跨进程接口、持久化字段、翻译与单元测试

Confidence Score: 4/5

合并前应修复登录项状态查询的参数匹配,否则新增的自启动开关无法可靠反映或控制 Windows 中的真实状态。

自定义参数登录项注册成功后,无参数查询仍会读取默认登录项状态,renderer 因而把已开启状态回退为关闭,并禁用依赖的托盘启动设置。

Files Needing Attention: apps/desktop/src/main/launchAtLogin.ts, apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx

Important Files Changed

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: 开关回退为关闭
Loading
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

Comment thread apps/desktop/src/main/launchAtLogin.ts Outdated
/** 读取系统登录项的当前状态。查询失败按「未启用」处理。 */
export function readLaunchAtLogin(app: LoginItemApp): boolean {
try {
return app.getLoginItemSettings().openAtLogin;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 登录项查询未匹配参数

在 Windows 用户开启开机自启动或重新进入设置页时,登录项以 --opened-at-login 参数注册,但这里未传入相同参数便读取 openAtLogin,因此查询的是默认空参数登录项并返回 false,导致开关回退为关闭且「开机启动时收起到托盘」持续不可用。

Suggested change
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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread apps/desktop/src/main/launchAtLogin.ts Outdated
/** 读取系统登录项的当前状态。查询失败按「未启用」处理。 */
export function readLaunchAtLogin(app: LoginItemApp): boolean {
try {
return app.getLoginItemSettings().openAtLogin;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread apps/desktop/src/main/launchAtLogin.ts Outdated
*/
export function readLaunchAtLogin(app: LoginItemApp): boolean {
try {
return app.getLoginItemSettings({ args: [OPENED_AT_LOGIN_FLAG] }).openAtLogin;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 使用 Windows 实际可启动状态读取开关

当用户在 Windows 任务管理器的“启动应用”中禁用 Cindy 时,Electron 的 openAtLogin 仍表示登录项已注册,只有 executableWillLaunchAtLogin 会同时考虑该登录项是否被系统禁用;这里因此仍把开关显示为开启,并让“收起到托盘”保持可用,用户也无法直接通过一次开启操作重新启用自启动。请在 Windows 设置流程中读取实际会执行的状态,并相应扩充 LoginItemApp 替身及回归测试。

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f0782c4, though not as a straight swap.

The two fields answer different halves of the question and neither is sufficient alone:

  • 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 per Electron's docs it "ignores the args option, 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>
@MagicLizi

Copy link
Copy Markdown
Contributor

@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>
@MagicLizi

Copy link
Copy Markdown
Contributor

这条 PR 被维护者确认门拦住了(产品:开机启动/托盘启动设置;同时碰到 preload 安全边界)。请等维护者在本 PR 上 Approve;需要修改就 Request Changes。讨论 issue:#3576

@MagicLizi MagicLizi added awaiting-discussion 等待维护者讨论(review-pr) touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) touches:security 改动碰到安全边界(review-pr 自动维护,仅展示) labels Aug 28, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 在 handler 中拒绝非 Windows 的登录项写入

即便已经校验 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 👍 / 👎.

Comment on lines +3723 to +3726
const startHidden = shouldStartHiddenInTray({
platform: process.platform,
argv: process.argv,
startInTrayOnLogin: readWindowBehaviorSettings().startInTrayOnLogin,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 避免把登录启动标记继承到主动重启

当当前进程确实由登录项启动时,--opened-at-login 会在 process.argv 中保留整个会话;而 updateService.ts 的更新通道切换及本文件的数据库清理都调用无参数 app.relaunch(),Electron 会默认把当前参数传给新实例。用户稍后主动执行这些重启时,新进程仍会在这里被判定为登录启动并再次隐藏到托盘,导致点击“重启”后窗口消失;这些 relaunch 路径需要过滤该一次性标记,或采用不会被应用内重启继承的启动来源判定。

Useful? React with 👍 / 👎.

if (typeof enabled !== 'boolean') {
throwIpcError('INVALID_PARAMS', 'startInTrayOnLogin required (boolean)');
}
writeStartInTrayOnLogin(enabled);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 将登录设置写入失败转换为统一 IPC 错误

当设置文件只读、磁盘已满或系统登录项 API 抛错时,这里和上面的 writeLaunchAtLogin() 会让底层异常直接穿过新 IPC handler;文件写入错误通常包含用户数据目录等内部绝对路径,而且不符合 renderer 依赖的 [IpcErrorCode] 协议。请捕获两个写入调用并通过 throwIpcError('INTERNAL', ...) 返回受控错误。

AGENTS.md reference: AGENTS.md:L103-L105

Useful? React with 👍 / 👎.

@MagicLizi

Copy link
Copy Markdown
Contributor

@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。

@MagicLizi

Copy link
Copy Markdown
Contributor

命中 UI 路径(apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-discussion 等待维护者讨论(review-pr) touches:core 改动碰到架构核心路径(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) touches:security 改动碰到安全边界(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants