Skip to content

[FEATURE]: desktop: close-to-tray option (keep the UI alive after closing the window) #58035

Open

Description

@Qianqian-chun
  • I have verified this feature I'm about to request hasn't been suggested before.

Correction: I initially ticked this box by mistake. This has been requested before — see "Existing work" below. The earlier requests were auto-closed for inactivity, so I opened a fresh one as the bot suggested in #27463 / #42349.

Describe the enhancement you want to request

The desktop app has no close-to-tray behaviour: closing the window quits the whole UI process. There is no setting to keep the window alive in the background, and Tray is never used in the main process. Clicking x therefore throws away the UI state (open tabs, unsent drafts, an in-flight streaming response) even though the background service keeps running.

Existing work

Current behaviour (verified on @opencode/desktop 2.1.2, Electron 44.9.1, Windows 11)

  • window-all-closed calls app.quit() on every non-darwin platform.
  • No Tray / setSkipTaskbar usage anywhere in the main or renderer bundles.
  • The renderer close control goes through the window.close RPC → BrowserWindow.close(). Windows only listen for ready-to-show, closed, focus and session-end — nothing intercepts close.
  • The v2 background service (opencode-cli serve --service) is an independent, detached process, so the UI is the only thing lost when the window closes.

Proposed behaviour

An opt-in setting (e.g. closeToTray in the desktop settings, default off so existing behaviour is unchanged):

  • Closing the window hides it and keeps the process running; a tray icon restores it.
  • Tray: left click toggles show/hide, context menu has Show / Hide / Quit OpenCode.
  • Explicit quit paths still exit normally: tray Quit, before-quit, OS session end, updater restart.
  • Re-launching the executable restores the existing hidden window through the already implemented second-instance handler, instead of starting a second process.

Suggested implementation

This can be done entirely with a small addition to the main process, without touching existing logic:

let quitting = false;
app.on("before-quit", () => { quitting = true; });

app.on("browser-window-created", (_event, win) => {
  win.on("session-end", () => { quitting = true; }); // OS shutdown / logoff
  win.on("close", (event) => {
    if (quitting || !settings.closeToTray) return;
    event.preventDefault();
    win.hide();
  });
});

app.whenReady().then(() => {
  const tray = new Tray(icon);
  tray.setToolTip("OpenCode");
  tray.on("click", () => {
    const win = BrowserWindow.getAllWindows()[0];
    if (win.isVisible() && win.isFocused()) win.hide();
    else { win.show(); win.focus(); }
  });
  tray.setContextMenu(Menu.buildFromTemplate([
    { label: "Show OpenCode", click: showWindow },
    { label: "Hide to tray", click: hideWindow },
    { type: "separator" },
    { label: "Quit OpenCode", click: () => { quitting = true; app.quit(); } },
  ]));
});

hide() removes the taskbar entry as well, and because the windows still exist, the existing window-all-closed → app.quit() path is never reached while the app is resident — so no changes to the lifecycle code are needed.

Verified locally

I prototyped exactly this patch against the extracted 2.1.2 bundle on Windows 11 and confirmed:

  • closing the window logs close intercepted -> hidden, the process stays alive and disappears from the taskbar;
  • launching the executable again raises the existing window through second-instance (no second process);
  • the tray icon is created successfully and a graceful quit still goes through the normal shutdown path.

Happy to turn this into a PR (setting + tray + tests) if the shape of the setting is acceptable to the maintainers.

Additional context

Activity

  1. opencode-agent commented on Oct 22, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up and for prototyping it on 2.1.2!

    To restate the request: you'd like an opt-in setting in the desktop app that hides the window to a system tray icon when you close it, instead of quitting the UI process. That way open tabs, drafts and streaming responses are kept, and you can still quit from the tray menu, at OS session end or when the updater restarts the app. The maintainers will consider it. Since you offered a PR, it may help to agree on the setting's shape with them here first.

    Related issues and PRs:

    This can't be done as an OpenCode plugin for now, because plugins can't reach the desktop app's main process, which handles the window and the tray.

  2. Qianqian-chun commented on Oct 22, 2026

    @Qianqian-chun
    Author

    Thanks — and good catch on the checklist. I've edited the issue: that box is a factual claim and I shouldn't have ticked it, since this was requested before (#18134, #27463, #35775, #42349, all closed without a resolution). The body now lists the earlier requests and the two related PRs.

    On the two PRs:

    • feat(desktop): close to tray instead of exiting #41520 is the v1 implementation of exactly this behaviour (resolveCloseAction + createTray, with unit tests). If porting it to v2 is the preferred path, I'm happy to do it and keep the same decisions it already encoded: only hide when the last window is closed, only on Windows/Linux, leave macOS close-to-Dock untouched, and let real quit paths (before-quit, tray Quit, signals) close normally. I'd add the setting on top so the behaviour stays opt-in.
    • feat(desktop): add menu-bar tray companion #52365 adds a tray companion with the session list. If that lands first, this request only needs the close interception plus the setting, and should reuse that tray instead of creating a second one.

    So the open question for maintainers: should I port #41520 to v2 now, or wait for #52365 and adapt on top of it? Either way I'm fine doing the work.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions