Skip to content

feat: notifications, login autostart, tray icon (PySide6) + docs wiki#1

Merged
tcsenpai merged 16 commits into
mainfrom
dev
May 19, 2026
Merged

feat: notifications, login autostart, tray icon (PySide6) + docs wiki#1
tcsenpai merged 16 commits into
mainfrom
dev

Conversation

@tcsenpai

@tcsenpai tcsenpai commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • Phase 1 — Desktop notifications: zero-dep watcher-errored notifier (notify-send / osascript / terminal-notifier), gated by `$BGO_NOTIFY`.
  • Phase 2 — Login autostart: `bgo autostart install|uninstall|status [--tray]` writes systemd-user units (Linux) or LaunchAgents (macOS) to run `bgo resurrect` (and optionally the tray) at every login.
  • Phase 3 — Tray icon (optional): `bgo tray` runs a PySide6 system-tray icon with native StatusNotifierItem on Wayland (KDE / Hyprland) and NSStatusItem on macOS. Gear+dot icon with status-driven color (green/red/gray), per-proc submenus, left/middle/right-click open the menu, Open-logs spawns `bgo logs -f` in a fresh terminal. PySide6 ships as an optional `[tray]` extra; first `bgo tray` run auto-detects installer (uv tool / pipx / pip) and offers to inject it.
  • fu #5 / #6 / #7: extracted `_term`, `_state`, `_proc`, `watcher` modules out of the 2070-line `bgo` script (now 1593); typed every remaining `cmd*` handler; wrote a 9-page docs wiki under `docs/` and slimmed the root README to a quickstart + index.

Highlights

  • 80 → 169 tests (+89).
  • README 417 → 106 lines; long-form moved into `docs/`.
  • New code (~2200 LOC) is strictly typed with full docstrings.
  • Replaced pystray (Xorg-only, broken under Wayland) with PySide6 in commit `2315c37` after deep research.
  • Caveman-review autofix pass on the new modules (commit `a7979b9`).
  • Mycelium followup #6 left open with a note: cmd_* handlers can be extracted in a later pass; the riskier 4-module extraction is done.

Verification

  • 169 / 169 tests pass on Python 3.13.
  • Live-verified on Fedora 44 Wayland:
    • `bgo tray` starts cleanly, dot color updates on status change, menus open on all three click types.
    • `bgo autostart install` writes `~/.config/systemd/user/bgo-resurrect.service`, `systemctl --user is-enabled` returns enabled; `bgo autostart uninstall` removes it cleanly.
    • `BGO_NOTIFY=all` fires a real desktop notification via `notify-send`.

Test plan

  • CI green on push.
  • Smoke: `uv tool install -e . --with PySide6`, then `bgo tray` shows tray icon.
  • Smoke: `bgo autostart install && bgo autostart status` reports installed.
  • macOS dev box: tray + autostart parity (LaunchAgent path).

Breaking changes

None. Existing commands (`start`, `stop`, `restart`, `status`, `logs`, `watch`, etc.) and all flags / env vars / exit codes are unchanged. The new `autostart` and `tray` subcommands are purely additive.

Summary by CodeRabbit

  • New Features

    • Added system tray UI, per-user autostart (resurrect/tray), CLI autostart/tray commands, and best-effort desktop notifications.
    • Improved watch-mode behavior for reliable restart/resurrect and clearer status/logging.
  • Documentation

    • Major docs overhaul: new docs/ landing pages covering installation, commands, watch mode, tray, autostart, notifications, architecture, FAQ, and contributing; README condensed.
  • Requirements

    • Minimum Python 3.10+; optional tray extra for PySide6.
  • Tests

    • Comprehensive test suites added for autostart, notify, tray, tray-install, watcher, and related helpers.
  • Chores

    • Updated ignore patterns for local artifacts.

Review Change Stack

tcsenpai added 13 commits May 19, 2026 11:52
Add bgo_cli._notify with a zero-dep, best-effort notifier that shells
out to notify-send (Linux), osascript or terminal-notifier (macOS), or
a user-supplied BGO_NOTIFY_CMD override. Gating env BGO_NOTIFY chooses
off | errors (default) | all.

Wire _notify_errored() into cmd_watcher_loop at all three errored
transitions (mode=stop, backoff exhausted, restart failure).

Fix latent bug in bgo_cli/__init__.py: importlib.machinery was used
without being imported, breaking dev-tree invocation.
Add bgo_cli._autostart with auto-detected backend:
- Linux  -> systemd user unit (~/.config/systemd/user/bgo-resurrect.service)
- macOS  -> LaunchAgent (~/Library/LaunchAgents/sh.discus.bgo.resurrect.plist)

Public API: install(target), uninstall(target), status(). Target is
'resurrect' (default) or 'tray'. Tray on Linux uses XDG desktop
autostart instead of systemd. macOS uses a second LaunchAgent plist.

Idempotent: re-running install overwrites; uninstall is safe when
nothing is installed. Atomic file writes (tmp + os.replace). Falls back
to launchctl 'load -w' when 'bootstrap' fails on older macOS or for
already-loaded plists.

28 unit tests, all platform-mocked: no real autostart entries are
written by the suite.
Add bgo_cli._tray (pystray + Pillow glue) and bgo_cli._tray_install
(auto-install fallback when the optional [tray] extra isn't present).

_tray splits pure menu logic (load_snapshots, build_menu_spec,
ProcSnapshot) from the pystray event loop so the testable surface
doesn't depend on the optional deps. Actions shell out to the 'bgo'
binary itself; no state-mutation logic is duplicated.

_tray_install detects the installer (uv tool / pipx / pip) from
sys.prefix and offers the matching dep-injection command:
- uv    -> uv tool install --upgrade --with pystray --with Pillow bgo-cli
- pipx  -> pipx inject bgo-cli pystray Pillow
- pip   -> python -m pip install --user pystray Pillow

User must confirm unless --auto-install or BGO_TRAY_AUTOINSTALL=1.
Aborts cleanly when the installer binary itself is missing.

After a successful install, cmd_tray re-execs sys.argv so the freshly
injected deps are picked up by a clean interpreter.

26 unit tests covering snapshot loading, menu rendering, poll
interval resolution, subprocess dispatch, and installer detection.
The pystray-bound _run_tray is intentionally not covered — it
requires the optional extra to be installed.
…features

- bgo autostart install|uninstall|status [--tray] [--json]
- bgo tray [--poll N] [--auto-install]
- both routed via lazy imports so missing optional deps degrade
  cleanly instead of failing at startup
- 'autostart' and 'tray' added to known_commands so direct-mode
  parsing doesn't misroute them as proc names
- pyproject: add [tray] optional extra (pystray>=0.19, Pillow>=10)
- README: three new sections (Desktop notifications, Autostart at
  login, Tray icon (optional)) with env vars, install commands per
  installer, and Wayland caveat
- notify: parse BGO_NOTIFY_CMD with shlex.split so quoted args group
  correctly (was: raw .split() lost quoting); malformed quotes now
  fall through to platform detection instead of producing an
  unrunnable argv.
- autostart: harden _resolve_bgo_binary to abspath via Path.resolve()
  even when shutil.which returns a relative entry; _launchctl_unload
  now actually surfaces failures instead of unconditionally returning
  True (still idempotent on 'Could not find specified service').
- tray: 'Start' menu entry was broken because 'bgo start <name>'
  without a command is rejected by the CLI. Route both Start (for
  stopped) and Restart (for online) through 'bgo restart <name>',
  which respawns with the stored command. _open_logs now reports
  fall-through failures on stderr instead of silently no-op'ing.
- bgo: cmd_tray re-exec resolves the installed entrypoint via
  shutil.which('bgo') instead of sys.argv[0], which may be the bare
  script path or 'python' after a uv/pipx injection.
- tray_install: honor $UV_TOOL_DIR for non-default uv tool roots;
  document the re-exec contract on ensure_installed.
- tests: +6 covering shlex quoting, malformed override fallthrough,
  launchctl unload fallback + missing-agent idempotency + real
  failure, and UV_TOOL_DIR detection. 144 -> 150 tests.
pystray.MenuItem._assert_action introspects the action's signature
and raises ValueError on any callable whose argspec has more than
the (icon, item) positional pair. Our previous form
    lambda _icon, _it, n=snap.name: run_bgo('restart', n)
binds the loop variable via a default-kwarg, which trips that
inspector even though the call site only passes two args.

Replace every menu-action lambda with a named closure factory
(bgo_action, open_logs_action, refresh_action, quit_action). Each
returns a fresh 2-arg callable that pystray accepts.

Reproduced as 'ValueError: <function ... lambda>' on first run of
'bgo tray' under uv tool install; now starts cleanly.
…support

pystray's default Xorg backend cannot dock under Wayland (it asserts
on _systray_manager being non-None), and its AppIndicator backend
needs distro typelibs plus a GNOME extension. Switching to PySide6
gives us a single library that speaks StatusNotifierItem natively on
Linux (KDE Plasma 6, Hyprland+waybar, sway, etc.) and uses native
NSStatusItem on macOS via Qt's Cocoa integration.

Changes:

- _tray.py rewritten: build_menu_spec / load_snapshots / ProcSnapshot
  / run_bgo are unchanged (still toolkit-agnostic, still tested).
  _run_tray now uses QApplication + QSystemTrayIcon + QMenu + QTimer.
  Icon is an embedded SVG rendered via QSvgRenderer at QIcon time, so
  we ship no binary assets. QTimer keeps polling on the GUI thread so
  there's no thread-unsafe menu mutation. We hold strong refs to
  every QAction/QMenu we create because Qt doesn't take ownership.
- Detection: if QSystemTrayIcon.isSystemTrayAvailable() returns False
  (typical on GNOME without the AppIndicator extension or on
  headless sessions), we print a per-distro install hint and exit 1
  instead of crashing.
- _tray_install: dep list trimmed from (pystray, Pillow) to
  (PySide6). All three installer paths (uv tool / pipx / pip)
  rewritten accordingly.
- pyproject [tray] extra is now just 'PySide6>=6.6'.
- README: rewritten tray section with platform support matrix and an
  explicit GNOME prerequisite section.
- Removed _select_pystray_backend / _print_appindicator_hint /
  _make_icon_image. Their tests are dropped — Qt handles backend
  selection internally and the SVG icon needs no Pillow pass.
- bgo (root script): updated docstrings / help text to mention
  PySide6 instead of pystray/Pillow.

GNOME Wayland still needs the AppIndicator shell extension — that's a
GNOME limitation, no Python library can paper over it. Documented in
README.

Live-verified on Fedora 44 Wayland: 'bgo tray' starts cleanly, Qt
event loop runs without error, no docking assertion.

Test count unchanged at 150 (6 pystray-backend-selection tests
removed; 6 Qt-glue paths are pragma: no cover by design).
…icon

Three tray UX improvements in one pass:

1. **Open logs** no longer opens the raw log file in $EDITOR. Instead
   it spawns 'bgo logs <name> -f' in a fresh terminal window so the
   user gets the project's formatted live tail. Linux probes a
   curated list of emulators (kitty, alacritty, wezterm, foot,
   ghostty, gnome-terminal, konsole, xfce4-terminal, tilix, xterm)
   and picks the first one on PATH. macOS uses AppleScript to spawn
   Terminal.app, with BGO_TERMINAL=iterm switching to iTerm2.
   BGO_TERMINAL also accepts arbitrary 'binary [exec-flag]' values
   for non-default emulators.

2. **Left-click activation**. By default QSystemTrayIcon only opens
   the context menu on right-click; left- and middle-click did
   nothing. Wired tray.activated to popup the menu at the cursor on
   Trigger / MiddleClick activation reasons. Same menu, both
   buttons.

3. **Gear+dot icon with status-driven dot color**:
   - green  (#3ddc84) when at least one proc runs and none errored
   - red    (#ff5252) when any proc is errored
   - gray   (#9e9e9e) when empty or all stopped
   Icon is regenerated only when the aggregate status changes (no
   wasted SVG rasterization on every 3s poll). Tooltip echoes the
   status string for monochrome themes that recolor icons.

Implementation notes:

- _icon_svg(color) builds the SVG on the fly; _aggregate_status
  reduces a snapshot list to one of {online, errored, idle}. Both
  are pure, unit-tested without Qt.
- build_menu was renamed to build_menu_from(snapshots) so rebuild
  loads procs once per tick and feeds them to both the icon-color
  decision and the menu construction.
- Added 11 new tests covering terminal resolution, log-opening on
  Linux/macOS, missing-terminal hint, aggregate status, and icon
  SVG rendering. 150 -> 166 tests.
…ft-click

Three reported issues addressed:

1. **Icon was gear teeth with blue center**. The original SVG used a
   single path with fill-rule=evenodd to cut the inner hole; Qt's
   QSvgRenderer + KDE Plasma's monochrome SNI repaint pass renders
   that as solid white gear, after which Plasma tints it accent-blue
   because it reads as monochrome. Redrawn as:
   - a stroked ring (the gear body),
   - 12 separate radial rect 'teeth',
   - a colored center dot.
   No path subtraction, no evenodd. Three explicit colors (white
   teeth + colored dot) means hosts no longer detect the icon as
   monochrome and stop recoloring it.

2. **No status colors in menu**. Qt QAction does not honor per-item
   foreground color across themes. Added Unicode status glyph prefix
   instead: '●  myproc  ·  online' / '○  myproc  ·  stopped' /
   '⚠  myproc  ·  errored'. Shape-based status communication works
   on every platform/theme; glyphs render reliably in KDE, GNOME,
   and macOS menus.

3. **Left-click didn't open the menu** on KDE Plasma. Replaced
   QMenu.popup() (async, gets eaten by some SNI hosts before paint)
   with QMenu.exec() (synchronous, force-shows). Added BGO_TRAY_DEBUG
   env var that logs the activation reason to stderr so users can
   diagnose host-specific quirks.

Tests: +4 (_status_glyph matrix). 166 -> 169.

README updated with full new tray behavior (icon dot colors, menu
glyphs, terminal-emulator probing for Open-logs, left/middle/right
click activation, BGO_TERMINAL + BGO_TRAY_DEBUG envs, autostart and
'bgo start -w' background examples). Also fixed the stale test
count (54 -> 169) and Python requirement (3.9+ -> 3.10+, matching
pyproject.toml since main's CI matrix dropped 3.9).
Begin modularization of the 2070-line root 'bgo' script per fu #6.
Source-of-truth for each concern moves into a focused module under
src/bgo_cli/; the root script re-imports the names so its public
namespace (and the test fixture's introspection of it) keeps working
unchanged.

Modules extracted in this pass:

- _term.py     (145 LOC) — COLORS, ANSI_RE, color(), strip_ansi(),
  truncate(), LEVEL_*, _detect_table_level(), GLYPHS, glyphs().
- _state.py    (113 LOC) — BGO_DIR, PROCS_DIR, LOGS_DIR, init_dirs(),
  proc_file(), log_path(), watcher_log_path(), watcher_log(),
  load_proc(), save_proc(), delete_proc(), load_all_procs().
- _proc.py     (191 LOC) — _is_zombie(), is_running(),
  _BLANK_PINFO, get_process_info(), get_process_info_batch(),
  _looks_like_command(), derive_name(), resolve_command(),
  kill_process().
- _watcher.py  (410 LOC) — WATCH_DEFAULTS, BACKOFF_SCHEDULE,
  TAIL_BYTES, _notify_errored, _resolve_watch_block,
  _default_watch_config, _spawn_watcher, _kill_watcher,
  _tail_stderr, _restart_proc_inplace, cmd_watcher_loop.

_spawn_watcher now resolves the bgo entrypoint via shutil.which
('bgo') with sys.argv[0] fallback, instead of os.path.abspath(
__file__). The latter would point at _watcher.py once relocated;
the former hits the user's installed CLI shim consistently across
uv tool / pipx / pip / repo-checkout invocations.

conftest.py now patches BGO_DIR / PROCS_DIR / LOGS_DIR on BOTH the
loaded bgo module and bgo_cli._state (when present) so tests that
mutate either binding stay consistent.

bgo script: 2070 -> 1593 LOC. Next pass: extract cmd_* handlers,
status rendering, interactive multiselect, and the argparse +
direct-mode CLI front-end.

All 169 tests pass unchanged.
fu #5 scope: documented + strictly typed Python code. New modules
(_notify, _autostart, _tray*, _term, _state, _proc, _watcher) already
meet this bar. This commit closes the gap for the cmd_* handlers and
top-level helpers that still live in the root bgo script.

Annotations added:

  cmd_start, cmd_stop, cmd_restart, cmd_watch, cmd_unwatch,
  cmd_status, cmd_logs, cmd_logs_follow, cmd_clean, cmd_resurrect,
  cmd_restart_stopped, cmd_restart_last, cmd_delete, cmd_autostart,
  cmd_tray, main

  -> every signature now (args: argparse.Namespace) -> int and
     carries a docstring describing inputs, behavior, and exit-code
     semantics.

  _clear_screen, _print_status_table, _print_proc_detail
  -> -> None return annotation and longer docstrings.

cmd_start / cmd_stop / cmd_restart / cmd_watch / cmd_status / cmd_logs
/ cmd_resurrect / cmd_delete also got expanded multi-paragraph
docstrings covering modes, side effects, and what each return code
means.

No behavior changes. 169 tests still pass.
fu #7 scope: GH-style wiki linking documentation. As lead I picked an
in-repo docs/ folder over GitHub's separate .wiki.git repo because:

- The wiki git repo is created via the GitHub web UI, can't ship with
  the main branch, and isn't reviewable in PRs.
- An in-repo docs/ folder renders natively in the GitHub web UI for
  navigation, supports MkDocs / Astro / GitHub Pages later, AND
  participates in normal review.

Structure:

  docs/README.md           — index linking every page
  docs/installation.md     — uv / pipx / pip / install.sh / manual
  docs/commands.md         — full CLI reference
  docs/watch-mode.md       — auto-restart + fast-crash policy
  docs/notifications.md    — desktop notifier backends + gating
  docs/autostart.md        — systemd-user / launchd integration
  docs/tray.md             — PySide6 tray, per-platform notes
  docs/architecture.md     — module dep graph, storage layout,
                              watcher protocol
  docs/faq.md              — common questions and gotchas
  docs/contributing.md     — dev setup, tests, patches, release

Pages link to each other consistently (e.g. tray.md references
autostart.md for 'run tray at login'; commands.md links to watch-mode
.md for tunable detail).

Root README rewritten as a slim quickstart + feature highlights + a
Documentation table that points into docs/. Went 417 -> 106 lines.
Duplicated content (full command tables, install matrices, tray
caveats) lives only in docs/ now.

169 tests still pass — no code touched in this commit.
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@tcsenpai has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 55 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5f7ff53-f53d-4ffe-ba30-e30a0259081d

📥 Commits

Reviewing files that changed from the base of the PR and between 3c947b2 and 25bc483.

⛔ Files ignored due to path filters (1)
  • .mycelium/mycelium.db is excluded by !**/*.db
📒 Files selected for processing (29)
  • .github/workflows/test.yml
  • .gitignore
  • README.md
  • bgo
  • docs/README.md
  • docs/architecture.md
  • docs/autostart.md
  • docs/commands.md
  • docs/contributing.md
  • docs/faq.md
  • docs/installation.md
  • docs/notifications.md
  • docs/tray.md
  • docs/watch-mode.md
  • pyproject.toml
  • src/bgo_cli/__init__.py
  • src/bgo_cli/_autostart.py
  • src/bgo_cli/_notify.py
  • src/bgo_cli/_proc.py
  • src/bgo_cli/_state.py
  • src/bgo_cli/_term.py
  • src/bgo_cli/_tray.py
  • src/bgo_cli/_tray_install.py
  • src/bgo_cli/_watcher.py
  • tests/conftest.py
  • tests/test_autostart.py
  • tests/test_notify.py
  • tests/test_tray.py
  • tests/test_tray_install.py
📝 Walkthrough

Walkthrough

Refactors the single-file bgo CLI into a modular bgo_cli package (state, term, proc, watcher), adds optional tray, autostart, and notification modules, updates the bgo CLI to re-export and wire handlers, and supplies comprehensive docs and tests.

Changes

Core Modularization & Optional Features

Layer / File(s) Summary
Project configuration
.gitignore, pyproject.toml, src/bgo_cli/__init__.py
Added .gitignore entries (.mycelium/, uv.lock), added tray extra (PySide6>=6.6), and imported importlib.machinery to support dynamic core loading.
State persistence foundation
src/bgo_cli/_state.py
JSON-per-process state storage under ~/.bgo/ with atomic writes, watcher log appending, delete/load helpers, and mutable module-level paths for test redirection.
Terminal rendering & glyphs
src/bgo_cli/_term.py
TTY/locale-aware rendering level detection (plain/normal/fancy), ANSI color wrapping/stripping, ANSI-aware truncation, and per-level glyph mapping.
Process inspection & lifecycle
src/bgo_cli/_proc.py
Cross-platform zombie detection, liveness checks, batched ps info collection, command heuristics, name derivation, command resolution, and kill escalation with permission handling.
Crash detection watcher sidecar
src/bgo_cli/_watcher.py
Detached per-process watcher loop implementing fast-crash policies (backoff/stop/retry), restart logic, watcher spawn/kill, stderr tail capture, and best-effort notifications.
Desktop notifications
src/bgo_cli/_notify.py
Zero-dependency notify(title, body, level) with $BGO_NOTIFY gating, backend resolution (env override, notify-send, osascript/terminal-notifier), AppleScript escaping, and safe subprocess execution.
Per-user autostart installation
src/bgo_cli/_autostart.py
systemd-user and launchd backends for resurrect and tray targets, template rendering, atomic writes, service-manager/bootstrap calls, uninstall, and status reporting.
Optional system tray UI
src/bgo_cli/_tray.py, src/bgo_cli/_tray_install.py
Pure model (snapshots, menu spec), subprocess/terminal helpers, SVG icon generation, lazy PySide6 runtime with QTimer refresh and menu actions; installer helper detects uv/pipx/pip and optionally injects PySide6.
CLI refactoring & new commands
bgo
Refactored to re-export module helpers; command handlers annotated with argparse.Namespace -> int; added cmd_autostart and cmd_tray with dynamic imports and argparse wiring.
Comprehensive documentation
docs/*, README.md
Added docs landing and pages for architecture, installation, commands, watch mode, notifications, autostart, tray, FAQ, and contributing; README restructured to point to docs/.
Test infrastructure & fixtures
tests/conftest.py
Updated fixture to sandbox BGO_DIR/PROCS_DIR/LOGS_DIR on both the loaded bgo module and bgo_cli._state.
Notification system tests
tests/test_notify.py
Parametrized tests for gating, backend resolution (override and platform fallbacks), AppleScript escaping, and notify() subprocess behavior.
Autostart installation tests
tests/test_autostart.py
Tests for backend detection, path resolution, template rendering, atomic writes, systemd/launchctl flows, fallback handling, and status reporting.
Tray UI & installer tests
tests/test_tray.py, tests/test_tray_install.py
Tests for snapshot loading, menu spec building, status/glyph mapping, poll interval resolution, bgo/terminal resolution, log-opening behavior, SVG icon uniqueness, and installer detection/injection flows.

🎯 4 (Complex) | ⏱️ ~75 minutes

"A rabbit hops through modular trees,
files split like leaves in springtime breeze.
State tucked safe, small icons glow,
watchers guard the processes below.
I nibble docs and tests with ease." 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main features being added: notifications, login autostart, tray icon with PySide6, and documentation wiki. It accurately reflects the primary changes in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bgo (1)

1191-1201: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor stop failure before deleting process state.

If kill_process(...) fails, the code still deletes the proc record (and optionally logs), which can leave a live unmanaged process.

Suggested fix
     if is_running(pid):
         if not getattr(args, "yes", False):
             confirm = input(f"Process '{name}' is running. Stop and delete? [y/N] ")
             if confirm.lower() != "y":
                 print("Cancelled.")
                 return 0
-        kill_process(pid, info.get("pgid"))
+        if not kill_process(pid, info.get("pgid")):
+            print(f"{color('red', '❌')} Failed to stop '{name}'. Aborting delete.")
+            return 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bgo` around lines 1191 - 1201, The current flow calls kill_process(pid,
info.get("pgid")) but proceeds to delete_proc(name, ...) regardless of whether
the stop succeeded; change the logic in the is_running branch to check the
result or catch exceptions from kill_process (e.g., capture a boolean/raise) and
only call delete_proc(name, keep_logs=keep_logs) when kill_process succeeded or
the process is confirmed not running (use is_running(pid) again after kill or
rely on kill_process return value); if kill fails, print an error and return
non-zero without deleting the proc record. Ensure you reference the existing
symbols is_running, kill_process, delete_proc, args, pid, name and
info.get("pgid") when making the change.
🧹 Nitpick comments (2)
docs/tray.md (1)

56-59: 💤 Low value

Add language identifier to fenced code block.

The code block showing the terminal emulator probe order should have a language identifier for consistency with markdown best practices. As per coding guidelines, markdownlint flagged this as MD040.

📝 Suggested fix
-```
+```text
 kitty → alacritty → wezterm → foot → ghostty →
 gnome-terminal → konsole → xfce4-terminal → tilix → xterm
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/tray.md around lines 56 - 59, The fenced code block showing the
terminal emulator probe order is missing a language identifier (MD040); locate
the triple-backtick block that contains "kitty → alacritty → wezterm → foot →
ghostty → gnome-terminal → konsole → xfce4-terminal → tilix → xterm" and add a
language tag such as "text" after the opening so the block becomestext
to satisfy markdownlint and maintain consistency.


</details>

</blockquote></details>
<details>
<summary>src/bgo_cli/_tray.py (1)</summary><blockquote>

`59-66`: _💤 Low value_

**Outdated docstring references pystray.**

The docstring mentions translating to "`pystray.Menu` objects" but the implementation uses PySide6/Qt. Consider updating for consistency.


<details>
<summary>Suggested fix</summary>

```diff
 `@dataclass`
 class MenuSpec:
     """Declarative description of the tray menu.

-    The ``run`` layer translates this into ``pystray.Menu`` objects;
-    tests inspect the spec directly. Keeping this layer
-    framework-agnostic means swapping pystray for another toolkit in
-    the future touches only ``_run_tray``.
+    The ``_run_tray`` layer translates this into Qt ``QMenu`` objects;
+    tests inspect the spec directly. Keeping this layer
+    framework-agnostic means swapping Qt for another toolkit in
+    the future touches only ``_run_tray``.
     """
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bgo_cli/_tray.py` around lines 59 - 66, The MenuSpec docstring still
mentions translating to "pystray.Menu objects" which is outdated; update the
docstring for MenuSpec to reference the actual toolkit used (PySide6/Qt) and how
the run layer translates the spec into QSystemTrayIcon/QMenu (or whatever
_run_tray uses), and remove or replace the pystray reference; mention the
translation happens in the _run_tray function/class so readers can find the
implementation.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @bgo:

  • Around line 1230-1243: The status branch currently ignores a possible --tray
    filter: when action == "status" and you call _autostart.status() the CLI always
    prints both "tray" and "resurrect" lines and the status subparser does not
    accept a --tray flag; update the status subparser to accept the same --tray
    option as other actions (so args.tray exists), and update the handler for action
    == "status" to respect args.tray (if args.tray is truthy only output/filter the
    "tray" line, if args.tray is False only output/filter non-tray lines, and when
    args.json is set still return the filtered structure from _autostart.status());
    touch the code paths referencing action, args, and _autostart.status() to apply
    the filter consistently (also replicate the same fix for the other occurrence
    noted around lines 1616-1625).
  • Around line 1276-1298: The broad except ImportError around "from bgo_cli
    import _tray" should be narrowed to catch ModuleNotFoundError as exc and only
    handle it when exc.name indicates missing GUI deps (e.g., "PySide6" or
    "shiboken6"); let other import or runtime errors in _tray propagate. In the
    fallback branch that calls _tray_install.ensure_installed and re-execs, preserve
    the original CLI arguments by passing [bgo_bin, *sys.argv[1:]] to os.execvp
    instead of hardcoding ["bgo_bin", "tray"]. Update the except clause and the
    os.execvp invocation near the imports of _tray/_tray_install and the
    ensure_installed call to implement these changes.

In @docs/architecture.md:

  • Around line 123-125: The sentence in docs/architecture.md contains a broken
    placeholder link "(#)" referencing "fu #6 partial completion" near the mention
    of extracting cmd_* handlers into bgo_cli._commands; either remove the link
    markup so it reads "fu #6 partial completion" without "(#)" or replace "(#)"
    with the actual GitHub issue/PR URL for fu #6; update the sentence accordingly
    to keep the reference correct and ensure cmd_* handlers and
    bgo_cli._commands remain mentioned.

In @docs/contributing.md:

  • Around line 53-56: Replace the broken placeholder link "(#)" in the "fu #5
    sweep" reference in the contributing doc: locate the sentence mentioning the
    root bgo script and cmd_* handlers (the "fu #5 sweep" phrase) and either
    remove the link markup entirely or replace "(#)" with the actual GitHub issue/PR
    URL; ensure the resulting text reads correctly (e.g., "fu #5 sweep" without link
    or "fu #5 sweep (https://...)" with the real link) and keep surrounding mention
    of cmd_* signatures and docstrings unchanged.

In @src/bgo_cli/_autostart.py:

  • Around line 323-327: The code currently calls _launchctl_unload(path) and then
    unconditionally deletes the plist and returns success; change this so that you
    capture the outcome or exception from _launchctl_unload in the block that checks
    path.exists(), and only call path.unlink(missing_ok=True) and return (True, "")
    if _launchctl_unload indicates success or a benign "already absent" result; if
    _launchctl_unload fails for any other reason, preserve the plist, return a
    failure tuple (False, "") or re-raise the error so the caller can
    handle it. Update the logic around _launchctl_unload and the subsequent
    path.unlink call in the _autostart removal flow to use the unload
    result/exception to decide whether to delete the file and report success.

In @src/bgo_cli/_proc.py:

  • Around line 87-88: The subprocess call building the ps command (where result =
    subprocess.run(...), using pid_arg) uses the GNU-only "--no-headers" flag;
    replace that with POSIX/BSD-compatible -o field= syntax by supplying a single -o
    argument that lists each column with trailing equals (e.g., pid, %cpu, %mem,
    etime) to suppress headers across platforms and remove the "--no-headers" token;
    update the argument list passed to subprocess.run and ensure any downstream
    parsing of result.stdout still strips/handles whitespace accordingly.

In @src/bgo_cli/_state.py:

  • Around line 61-69: The loader functions (load_proc and load_all_procs)
    currently accept any JSON value; update load_proc (and similarly load_all_procs)
    so after parsing the file from proc_file(name) you validate the result is a
    mapping (instance of dict) and return None for any non-dict JSON or errors;
    i.e., if json.loads(...) returns a list/str/number/etc., treat it as corrupt and
    return None instead of returning the raw value that downstream code will call
    .get(...) on.
  • Around line 81-83: The current atomic-write uses a fixed tmp path (tmp =
    pf.with_suffix(pf.suffix + ".tmp") / tmp.write_text and os.replace(tmp, pf))
    which is racy for concurrent writers; change to create a unique temp file in the
    same directory (e.g. via tempfile.NamedTemporaryFile or tempfile.mkstemp with
    dir=pf.parent and a unique suffix), write the JSON to that unique temp path,
    fsync if needed, close the handle, then call os.replace(unique_tmp, pf) to
    atomically swap; ensure you reference the same variables (pf, tmp or unique_tmp)
    and remove/cleanup the temp on error.

In @src/bgo_cli/_term.py:

  • Around line 41-46: The color() function currently appends the reset code even
    when the provided name is unknown; update it so that after the TTY check it
    first verifies the color key exists in the COLORS mapping (e.g. using "if name
    not in COLORS: return str(text)"), and only when the key is present wrap the
    text with COLORS[name] and append COLORS['reset']; reference the color()
    function, the COLORS dict, and sys.stdout.isatty() when making the change.
  • Around line 53-58: truncate() can produce results longer than width when width
    < 3; update the function (truncate) to handle small widths explicitly: after
    computing plain = strip_ansi(s) and detecting len(plain) > width, if width <= 0
    return "" (or empty string), if width <= 3 return the ellipsis trimmed to width
    (use "..."[:width]) so the result length never exceeds width, otherwise keep the
    existing behavior of returning s[: width - 3] + "..." for wider columns;
    reference truncate and strip_ansi to locate the change.

In @src/bgo_cli/_tray_install.py:

  • Around line 109-111: The docstring for ensure_installed incorrectly references
    "pystray + PIL" while the function actually ensures PySide6 is
    importable/installed; update the ensure_installed docstring to accurately
    describe that it checks for and installs PySide6 (and any other real runtime
    deps used by functions in this module), mention the optional auto parameter
    behavior, and remove leftover references to pystray/PIL so the docstring matches
    the implementation in ensure_installed.

In @src/bgo_cli/_watcher.py:

  • Around line 139-151: The code opens a log file handle (wlog via
    watcher_log_path) before calling subprocess.Popen and in the except path returns
    without guaranteeing the file is closed; ensure the file descriptor is always
    closed on all failure paths by either using a context manager (with
    open(watcher_log_path(name), "a") as wlog:) around the subprocess.Popen call or
    by adding explicit wlog.close() in every except/return path; apply the same fix
    to the other spawn path referenced in the file (the block around lines 202-225)
    so watcher_log, _bgo_entrypoint, and any failing subprocess.Popen invocation
    never leak the wlog file descriptor.

In @tests/conftest.py:

  • Around line 33-57: The test imports fail during collection because the test
    package path ("src/") is only added inside the bgo fixture; move the sys.path
    insertion to module import/collection time by adding a top-level
    sys.path.insert/append that adds the project's "src" directory before any test
    code or helper functions run (i.e., before calling _load_bgo() or defining the
    bgo fixture). Ensure this change happens in tests/conftest.py at the module
    scope (so it runs on import), and keep references to _load_bgo(), bgo_dir,
    BGO_DIR, PROCS_DIR, and LOGS_DIR unchanged so the existing fixture logic still
    patches those symbols.

Outside diff comments:
In @bgo:

  • Around line 1191-1201: The current flow calls kill_process(pid,
    info.get("pgid")) but proceeds to delete_proc(name, ...) regardless of whether
    the stop succeeded; change the logic in the is_running branch to check the
    result or catch exceptions from kill_process (e.g., capture a boolean/raise) and
    only call delete_proc(name, keep_logs=keep_logs) when kill_process succeeded or
    the process is confirmed not running (use is_running(pid) again after kill or
    rely on kill_process return value); if kill fails, print an error and return
    non-zero without deleting the proc record. Ensure you reference the existing
    symbols is_running, kill_process, delete_proc, args, pid, name and
    info.get("pgid") when making the change.

Nitpick comments:
In @docs/tray.md:

  • Around line 56-59: The fenced code block showing the terminal emulator probe
    order is missing a language identifier (MD040); locate the triple-backtick block
    that contains "kitty → alacritty → wezterm → foot → ghostty → gnome-terminal →
    konsole → xfce4-terminal → tilix → xterm" and add a language tag such as "text"
    after the opening so the block becomestext to satisfy markdownlint and
    maintain consistency.

In @src/bgo_cli/_tray.py:

  • Around line 59-66: The MenuSpec docstring still mentions translating to
    "pystray.Menu objects" which is outdated; update the docstring for MenuSpec to
    reference the actual toolkit used (PySide6/Qt) and how the run layer translates
    the spec into QSystemTrayIcon/QMenu (or whatever _run_tray uses), and remove or
    replace the pystray reference; mention the translation happens in the _run_tray
    function/class so readers can find the implementation.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `ff0bddd5-a054-4f4a-ba50-ed07d3bd7819`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3c947b21ba532ad1431324c973a22295704aa0bd and 2fedd2aceb5ae2aac15db9b69a8f72ddefa1c10e.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `.mycelium/mycelium.db` is excluded by `!**/*.db`

</details>

<details>
<summary>📒 Files selected for processing (28)</summary>

* `.gitignore`
* `README.md`
* `bgo`
* `docs/README.md`
* `docs/architecture.md`
* `docs/autostart.md`
* `docs/commands.md`
* `docs/contributing.md`
* `docs/faq.md`
* `docs/installation.md`
* `docs/notifications.md`
* `docs/tray.md`
* `docs/watch-mode.md`
* `pyproject.toml`
* `src/bgo_cli/__init__.py`
* `src/bgo_cli/_autostart.py`
* `src/bgo_cli/_notify.py`
* `src/bgo_cli/_proc.py`
* `src/bgo_cli/_state.py`
* `src/bgo_cli/_term.py`
* `src/bgo_cli/_tray.py`
* `src/bgo_cli/_tray_install.py`
* `src/bgo_cli/_watcher.py`
* `tests/conftest.py`
* `tests/test_autostart.py`
* `tests/test_notify.py`
* `tests/test_tray.py`
* `tests/test_tray_install.py`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread bgo
Comment thread bgo
Comment thread docs/architecture.md Outdated
Comment thread docs/contributing.md Outdated
Comment thread src/bgo_cli/_autostart.py Outdated
Comment thread src/bgo_cli/_term.py
Comment thread src/bgo_cli/_term.py
Comment thread src/bgo_cli/_tray_install.py
Comment thread src/bgo_cli/_watcher.py
Comment thread tests/conftest.py
tcsenpai added 2 commits May 19, 2026 15:04
Qt's app.exec() blocks in a native select() loop, so Python signal
handlers never run. Install handlers that call app.quit() and add a
200ms no-op QTimer to wake the interpreter often enough to deliver
pending signals.
CI:
- workflow now installs the package (`pip install -e .[dev]`) so test
  modules can `import bgo_cli` without a sys.path hack on disk
- conftest.py also inserts `src/` at collection time as a belt-and-
  braces fallback for local pytest runs from the repo root

Quality:
- _state.save_proc: unique tmp file via tempfile.mkstemp; previous
  shared `*.tmp` path was racy under concurrent writers
- _state.load_proc / load_all_procs: reject non-mapping JSON so a
  corrupt list/string can't crash callers that do `.get(...)`
- _proc.get_process_info_batch: use POSIX-portable `pid=,%cpu=,...`
  header suppression; `--no-headers` is GNU-only and broke macOS ps
- _term.color: unknown names now return the text untouched (was
  appending a stray reset code, violating the documented contract)
- _term.truncate: clamp at narrow widths so the 3-char ellipsis can
  no longer overflow into width<3 columns
- _watcher: open log handles get closed via `finally`, so repeated
  spawn failures don't leak FDs
- _autostart.uninstall: propagate launchctl unload errors instead of
  silently deleting the plist and orphaning the agent
- _tray_install.ensure_installed: docstring referenced pystray/PIL
  (leftover from pre-PySide6 migration)

CLI:
- `autostart status --tray` is now wired through (subparser flag +
  handler), matching the help text
- `cmd_tray` re-exec forwards `sys.argv[1:]` so `--poll` survives

Docs:
- replace `(#)` placeholder links in architecture.md / contributing.md
  with real issue URLs

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bgo_cli/_tray.py`:
- Around line 99-108: The function load_snapshots currently sorts JSON files by
filename before calling _load_one, which can produce the wrong order if a
ProcSnapshot's internal name differs from the filename; change load_snapshots
(and use ProcSnapshot.name) so it first loads all snapshots via _load_one into
out and then return them sorted by each snapshot.name (e.g., using sorted(out,
key=lambda s: s.name)) while still filtering out None results.
- Around line 284-297: The AppleScript string is vulnerable because `quoted`
(produced from `follow_cmd` using `shlex.quote`) is interpolated directly into
`script`; escape the AppleScript layer by escaping backslashes and double quotes
in `quoted` before interpolation (e.g. replace "\" with "\\\\" and `"` with
"\\\"") and use that escaped value in both `if use_iterm:` and `else:` branches
when building `script` (references: `follow_cmd`, `shlex.quote`, `quoted`,
`use_iterm`, and `script`). Ensure the escaping happens immediately after
`quoted = ...` so all subsequent uses are safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b39cb2ef-d2d7-4a8a-9891-a33f1a80738e

📥 Commits

Reviewing files that changed from the base of the PR and between 2fedd2a and 554cd4d.

📒 Files selected for processing (1)
  • src/bgo_cli/_tray.py

Comment thread src/bgo_cli/_tray.py Outdated
Comment thread src/bgo_cli/_tray.py
- load_snapshots: sort after parsing so order follows the snapshot's
  stored `name` field (filename can drift from name after rename)
- _open_logs_darwin: escape backslashes + double quotes in the
  shell-quoted command before interpolating into the AppleScript
  string literal — paths containing either would otherwise terminate
  the AppleScript literal early
@tcsenpai

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@tcsenpai tcsenpai merged commit be0e25e into main May 19, 2026
10 checks passed
@tcsenpai tcsenpai deleted the dev branch May 19, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant