Window, Menubar & Lifecycle
Tray, single-instance, window state, launch at login, and orphan reclamation
The window and the engine process behave like a proper Mac app: closing the window puts it away, a second launch focuses the first, and a leftover engine is adopted rather than duplicated.
from praisonaiagents import Agent
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
)
# The app keeps this agent's engine alive in the background,
# even when you close the window.
agent.start("Still here?")Quick Start
- Close, don't quit
Hit the red button — the window hides and the menubar item keeps the engine running.
- Reopen from the tray
Click the menubar glyph to bring the window back; the engine never restarted.
- Quit for real
Use Quit PraisonAI from the tray (or
⌘Q) to stop the engine cleanly.
Menubar Tray
A menubar item hosts the app’s status and controls, so closing the window means “put it away” rather than “quit”.
| Item | Action |
|---|---|
| Open PraisonAI | Bring the window forward |
| Engine: … | Live status label (starting…, then the running state) |
| Settings… | Open settings (⌘, on macOS) |
| Quit PraisonAI | Stop the engine and exit (⌘Q on macOS) |
Single-Instance
Launching PraisonAI a second time focuses the existing window instead of opening a rival shell.
Registered via tauri-plugin-single-instance, and first — the guard runs before anything else touches the lockfile or the engine, so two shells never race over one engine.
Draggable Titlebar
The header carries data-tauri-drag-region, and the core:window:allow-start-dragging permission is granted in src-tauri/capabilities/default.json.
Window Geometry Persistence
Position and size are saved as the window moves, resizes, and closes, via tauri-plugin-window-state.
Saves are coalesced during a drag (one file write after a short pause), and geometry is also written on close and exit — because a hidden window never fires a real close, and a signal kill runs neither handler.
Launch at Login
Turning on Open at login asks the OS to register the app to launch at sign-in. The toggle now reflects what the OS actually did, not what you asked for.
| State | What settings.json stores | What the UI shows | When you see it |
|---|---|---|---|
| Registered | launch_at_login: true | Toggle on | Installed .app bundle on macOS |
| Not registered | launch_at_login: false | Toggle off | Never turned it on, or turned it off |
| Refused by the OS | launch_at_login: false | Toggle snaps back to off, inline warning shows the reason | Windows, Linux, and macOS when the app isn’t running from an .app bundle (e.g. cargo run, dev checkout) |
Orphan Reclamation
If a previous run left an engine alive — a crash or a signal kill skips the orderly quit — the app adopts it instead of starting a second one beside it.
The lockfile (engine.lock) is bumped to LOCK_FORMAT_VERSION = 2 and distinguishes every outcome:
| Lock state | Decision |
|---|---|
| Absent | Spawn a fresh engine |
| Present, healthy, same venv | Adopt the running engine on its saved port |
| Present, unhealthy | Kill and respawn, then remove the stale lock |
| Corrupt / incompatible | Treated as nothing adoptable — spawn |
Best Practices
Close to keep the engine warm
The engine takes seconds to start. Closing the window hides it and keeps the process alive, so the next open is instant.
Quit from the tray to stop the engine
The red button hides; Quit PraisonAI (or ⌘Q) exits and reaps the engine. Verify with pgrep -f server.py after quitting.
Delete the lockfile only when the app is fully quit
If the app is stuck, quit it, remove engine.lock from the data directory, and relaunch. Deleting it while the app runs invites a duplicate engine.