A desktop application must choose whether closing its window also stops its process. ChatCrystal currently hides the window in the tray. Restoring bounds, showing the window, and quitting have separate implementations. This article reads the pinned main process and tray module; their presence does not establish a complete experience across platforms. For the main/preload relationship, see Electron basics.
The menu follows the connection mode
createTray destroys the previous instance, then creates a 16×16 nativeImage and Tray. Its inputs include a mode and action callbacks, rather than one fixed localhost destination.
| Mode | Browser and search destination | Actions always included |
|---|---|---|
| onboarding | No connected destination; both entries are omitted | Show window, reconnect, check for updates, quit |
| local | localBaseUrl |
Same actions |
| cloud | cloudBaseUrl |
Same actions |
Search loads the destination’s /search; opening the browser uses shell.openExternal. Double-click and the show-window item call showMainWindow, which restores a minimized window before show and focus. Search directly calls show, focus, and loadURL without that explicit restore step. The entry points therefore do not all implement the same restoration sequence.
16×16 is this revision’s resize choice; it does not make every source image sharp on every platform. The Electron Tray documentation distinguishes platform events and icon requirements, and marks double-click as macOS/Windows. Linux desktop environments need separate acceptance checks. Registering an event in source does not establish identical platform behavior.
close saves state before deciding to hide
This excerpt is the actual close handler. It depends on existing main-process variables and functions; it is not a standalone application:
win.on('close', (event) => {
saveWindowState(win);
if (!isQuitting) {
event.preventDefault();
win.hide();
}
});
For an ordinary close, isQuitting is false: the handler saves state and hides the window, leaving the service and process running. The tray quit item calls app.quit(). The first before-quit intercepts quitting and sets the flag; the second quit, after cleanup, proceeds. The window-all-closed handler does not call quit.
Hiding preserves state in the running process. It does not mean all data has reached disk. Window JSON and the knowledge database use different persistence paths; successfully reopening a hidden window does not verify database persistence.
Normal bounds and position recovery are separate checks
The file is app.getPath('userData')/window-state.json. It stores x, y, width, height, and isMaximized. Resize/move events outside maximized state update lastNormalBounds; saving a maximized window prefers that cache and otherwise falls back to getBounds.
This preserves normal bounds that have already been observed. The cache is not initialized with a value, and the state is not a complete fullscreen/minimize model. Electron also offers getNormalBounds. The current code uses a cache; an available API and an implemented behavior are different facts.
A read or JSON parse failure returns the default 1280×800, non-maximized state. After a successful read, non-finite width/height fall back to defaults and dimensions are clamped to at least 1280×800. These are the current default and minimum dimensions; the original draft’s 900×600 minimum is outdated. Save errors are swallowed: persistence is best effort, with no success notification or retry.
Position validation checks only the saved top-left point against each display’s bounds, allowing 50 of tolerance on its left and top edges. If none matches, x/y are cleared so the system chooses a position. It does not check the whole window’s intersection with workArea or clamp its dimensions to a small display. Coordinates also lack a complete runtime schema. Disconnecting a secondary display therefore does not guarantee that the entire window becomes visible.
Quitting has an order and a timeout
gracefulShutdown calls kill on an active remote-import worker and clears its reference, then awaits local service shutdown and destroys the tray. It does not wait for the worker’s exit event. See Fastify production static service for the database and HTTP shutdown order.
before-quit starts a 10-second timer that calls app.exit(1) on timeout. Whether cleanup succeeds or throws, finally clears the timer and calls quit again. While the event loop can run, the timer limits waiting for asynchronous cleanup. It cannot preempt synchronous disk writes that block the main thread, or establish that every write completed. requestSingleInstanceLock and second-instance handling prevent another window from opening for the same Electron application. They do not lock a SQLite file against simultaneous CLI and desktop writers.
Verify scenarios beyond a visible window
Using an isolated test profile, record ordinary close followed by tray restoration; minimized-window restoration through show, double-click, and search separately; restarting after maximized close and then unmaximizing; starting after disconnecting a secondary display; a malformed or unwritable state file; and cleanup that fails or exceeds 10 seconds.
These are proposed acceptance scenarios, not desktop experiments claimed by this article. Record visibility, state-file changes, and exit code separately so that one result does not stand in for the others.