Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions docs/superpowers/plans/2026-08-28-windows-server-tray.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Windows Server Tray Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a Windows-native tray icon and controls to `codeg-server.exe`, and add an “Open Logs Folder” action to the existing desktop tray.

**Architecture:** Add a `server_tray` library module compiled only for Windows. It owns a `tray-icon` + `tao` event-loop thread, embeds the existing PNG/ICO assets, and sends `OpenWeb`, `OpenLogs`, and `Quit` commands to the server runtime. The server wraps `axum::serve` with graceful shutdown driven by the tray’s `Quit` command. The existing Tauri tray menu receives one additional log item.

**Tech Stack:** Rust 2021, Axum, Tokio, `tray-icon` 0.21, `tao` 0.34, `open` 5, Tauri 2 desktop tray.

---

### Task 1: Add testable tray command routing

**Files:**
- Create: `src-tauri/src/server_tray.rs`
- Modify: `src-tauri/src/lib.rs`
- Modify: `src-tauri/Cargo.toml`
- Modify: `src-tauri/Cargo.lock`

- [ ] **Step 1: Define the Windows-only command API and non-Windows stub**

Expose `TrayCommand`, `TrayHandle`, and `start(url, logs_dir)` from `server_tray`. On non-Windows targets, `start` returns `Ok(None)` so server builds remain headless and testable.

- [ ] **Step 2: Add target-specific dependencies**

Add `tray-icon = "0.21.3"`, `tao = "0.34.5"`, and `open = "5"` under `target.'cfg(target_os = "windows")'.dependencies`. Keep the existing Tauri tray dependency and server dependency graph separate through `cfg`.

- [ ] **Step 3: Add pure tests for URL and log-path propagation**

Test the public `TrayConfig`/command construction without creating a native event loop. The test must assert the exact URL and absolute log directory passed to the platform starter.

- [ ] **Step 4: Run the focused library test**

Run from `src-tauri`:

```text
cargo test --no-default-features server_tray
```

Expected result: the non-Windows stub tests pass; on a Windows runner the same tests compile with the native module.

### Task 2: Implement the Windows tray event loop

**Files:**
- Modify: `src-tauri/src/server_tray.rs`
- Add binary asset reference: `src-tauri/icons/icon.png`

- [ ] **Step 1: Build the event-loop thread**

Create a `tao::EventLoop<UserEvent>` on a dedicated thread, forward `TrayIconEvent` and `MenuEvent` through `EventLoopProxy`, and create the tray icon during `StartCause::Init`.

- [ ] **Step 2: Build the menu and icon**

Use `muda::Menu`, `MenuItem`, and `PredefinedMenuItem` through `tray_icon::menu`. Use `include_bytes!("../icons/icon.png")` plus the existing `image` decoder to construct `tray_icon::Icon`. Menu ids must be stable constants: `server-tray:open-web`, `server-tray:open-logs`, and `server-tray:quit`.

- [ ] **Step 3: Route interactions**

Handle `TrayIconEvent::DoubleClick` with the left button by sending `OpenWeb`. Handle menu ids by sending `OpenWeb`, `OpenLogs`, or `Quit`; on `Quit`, drop the tray icon and set the event loop control flow to exit.

- [ ] **Step 4: Launch external applications safely**

Perform `open::that(url)` and `open::that(logs_dir)` inside the tray thread, logging failures with `tracing::warn!`. Callback failures must not panic or terminate the tray loop.

### Task 3: Connect the tray to server startup and graceful shutdown

**Files:**
- Modify: `src-tauri/src/bin/codeg_server.rs`
- Modify: `src-tauri/src/web/mod.rs`

- [ ] **Step 1: Add a server-owned shutdown sender**

Create a `tokio::sync::oneshot` channel after the listener binds. Start the tray with the final advertised URL and `codeg_logs_root()` path, then spawn a blocking receiver task that resolves when the tray sends `Quit`.

- [ ] **Step 2: Use Axum graceful shutdown**

Replace the bare `axum::serve(listener, router).await` with `with_graceful_shutdown` awaiting either the tray receiver or the existing process shutdown condition. Trigger `state.web_server_state.shutdown_signal()` before the graceful future resolves so WebSocket handlers drain consistently.

- [ ] **Step 3: Keep startup resilient**

Log tray initialization errors and continue serving HTTP. Keep the `TrayHandle` alive until `axum::serve` returns, then drop it before the existing office-watch cleanup.

- [ ] **Step 4: Run server checks**

Run:

```text
cargo check --no-default-features --bin codeg-server
cargo test --no-default-features --bin codeg-server --lib
```

Expected result: Linux/macOS compile without the tray dependency and existing server tests pass.

### Task 4: Extend the desktop tray menu

**Files:**
- Modify: `src-tauri/src/commands/windows.rs`
- Modify: `src-tauri/src/lib.rs`

- [ ] **Step 1: Add a stable “Open Logs Folder” menu id and localized labels**

Extend `TrayLabels` for all existing locales and add `TRAY_MENU_ID_LOGS`.

- [ ] **Step 2: Add the menu item in install and refresh paths**

Insert the log item between “Show Workspace” and the separator in both `install_tray_icon` and `refresh_tray_menu`.

- [ ] **Step 3: Dispatch the action**

Handle `TRAY_MENU_ID_LOGS` in the app-wide `on_menu_event` callback by invoking the existing `open_logs_dir` command/core implementation and logging any error.

- [ ] **Step 4: Run desktop checks**

Run `cargo check --features tauri-runtime` and the existing Rust test command for the desktop feature.

### Task 5: Verify Windows release packaging

**Files:**
- Modify: `.github/workflows/release.yml`
- Modify: `.github/workflows/test.yml` only if a focused Windows check is needed

- [ ] **Step 1: Verify the release job carries the embedded icon**

Keep the existing Windows packaging paths unchanged; the icon is inside the PE resource and requires no extra `web/` file.

- [ ] **Step 2: Add a Windows smoke assertion**

After building `codeg-server.exe`, use a PowerShell PE/resource inspection available on the hosted runner to assert the binary has an icon resource. Keep the existing executable-presence and `codeg-mcp --help` checks.

- [ ] **Step 3: Run formatting and diff checks**

Run `cargo fmt --check`, `git diff --check`, and inspect the final release diff for unrelated changes.

- [ ] **Step 4: Commit implementation changes**

```text
git add src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/src/server_tray.rs src-tauri/src/bin/codeg_server.rs src-tauri/src/web/mod.rs src-tauri/src/commands/windows.rs src-tauri/src/lib.rs .github/workflows/release.yml
git commit -m "feat(server): add Windows tray controls"
```

85 changes: 85 additions & 0 deletions docs/superpowers/specs/2026-08-28-windows-server-tray-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Windows Server Tray Design

## Goal

Give the standalone `codeg-server.exe` Windows release a native system-tray
presence with the same Codeg icon used by the web and desktop products. The
tray is a control surface for the headless server, not a hidden desktop window.

## Behavior

### Standalone server

- Tray icon: `src-tauri/icons/icon.ico` embedded in the Windows executable.
- Double click opens the server Web UI at the resolved local address
(`HOST:PORT`). A single click has no side effect, matching normal Windows
tray expectations and avoiding two browser launches for one double click.
- Menu item `Open Web Console` opens the same URL.
- Menu item `Open Logs Folder` opens the directory returned by
`paths::codeg_logs_root()` in Windows Explorer. The directory contains the
rotating `codeg-server.<date>.log` files.
- Menu item `Quit Codeg Server` requests graceful shutdown through the existing
shutdown signal, allowing the HTTP server and log guard to flush before the
process exits.
- Tray initialization failure is logged and does not prevent the HTTP server
from starting.

### Desktop application

- Keep the existing left-click behavior: show and focus the native workspace.
- Add `Open Logs Folder` to the existing tray menu.
- Keep `Show Workspace` and `Quit Codeg` unchanged.
- Desktop and server use the same icon asset, but their activation targets are
intentionally different because only the desktop build owns a native window.

## Architecture

The server build does not enable Tauri, so its tray implementation must live in
a small Windows-only module compiled into `codeg-server` under
`cfg(target_os = "windows")`. A native tray dependency with a message-loop
thread owns the icon and menu. The tray thread sends typed commands over a
channel to the server's async runtime:

```text
Windows tray callback
|
v
TrayCommand channel ----> server runtime
|-- open browser
|-- open Explorer
`-- trigger shutdown
```

The tray thread is started after the bind address and log directory are
resolved, so menu actions always use the actual runtime configuration. It is
stopped by the same shutdown path as the HTTP server. Non-Windows builds
compile a no-op starter and retain their current behavior.

The desktop menu is extended in the existing Tauri tray module. Its log action
calls the existing `open_logs_dir` command, while server mode opens the path
directly from the native tray handler because there is no Tauri command layer.

## Data and error handling

- The server passes a fully formed Web UI URL and an absolute logs directory to
the tray starter; no environment is read from callbacks.
- Browser and Explorer launch failures are written to the normal server log,
while the menu callback remains responsive.
- A closed command channel means the server is shutting down; callbacks become
no-ops and never panic.
- Tray startup errors are non-fatal and include the target platform and error
detail in the log.
- The icon is embedded at compile time so packaged releases do not depend on a
neighboring `web/` file or the user's working directory.

## Testing and release checks

- Unit-test command routing and URL/path propagation without creating a real
tray on non-Windows CI.
- Run `cargo check --no-default-features --bin codeg-server` on all existing
targets.
- Extend the Windows release smoke test to assert the server binary is present;
manual Windows validation confirms icon visibility, single/double activation,
opening the logs directory, and graceful quit.
- Verify the existing desktop tray tests and `cargo check` remain unchanged on
non-Windows targets.
24 changes: 24 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ required-features = []

[build-dependencies]
tauri-build = { version = "2", features = [], optional = true }
embed-resource = "3.0.6"

[dependencies]
tauri = { version = "2", features = ["macos-private-api", "tray-icon"], optional = true }
Expand Down Expand Up @@ -156,6 +157,9 @@ mac-notification-sys = "0.6"
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Storage_FileSystem", "Win32_Foundation", "Win32_System_Threading"] }
junction = "1"
tray-icon = "0.21.3"
tao = "0.34.5"
open = "5"

[target.'cfg(unix)'.dependencies]
libc = "0.2"
Expand Down
13 changes: 13 additions & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
fn main() {
#[cfg(target_os = "windows")]
{
println!("cargo:rerun-if-changed=windows/codeg-server.rc");
println!("cargo:rerun-if-changed=icons/icon.ico");
embed_resource::compile_for(
"windows/codeg-server.rc",
["codeg-server"],
embed_resource::NONE,
)
.manifest_required()
.expect("failed to embed the codeg-server Windows icon");
}

#[cfg(feature = "tauri-runtime")]
{
ensure_sidecar_placeholder();
Expand Down
46 changes: 45 additions & 1 deletion src-tauri/src/bin/codeg_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,52 @@ async fn async_main() -> ExitCode {
tracing::info!(" {}", addr);
}

// Standalone Windows builds have no Tauri window to own a tray icon. Keep
// the native tray alive for the lifetime of the server and use its quit
// action to enter Axum's graceful shutdown path.
#[cfg(target_os = "windows")]
let tray_quit_rx = {
let tray_handle = match codeg_lib::server_tray::start(
addresses
.first()
.cloned()
.unwrap_or_else(|| format!("http://{}:{}", advertised_host, actual_port)),
codeg_lib::commands::logging::open_logs_dir_core()
.map(PathBuf::from)
.unwrap_or_else(|err| {
tracing::warn!("[Tray] failed to prepare log directory: {}", err.message);
codeg_lib::paths::codeg_logs_root()
}),
) {
Ok(handle) => handle,
Err(err) => {
tracing::warn!("[Tray] disabled: {err}");
None
}
};
tray_handle.map(|handle| handle.into_quit_receiver())
};

// Start serving
if let Err(e) = axum::serve(listener, router).await {
#[cfg(target_os = "windows")]
let shutdown_signal = state.web_server_state.shutdown_signal();
#[cfg(target_os = "windows")]
let graceful_shutdown = async move {
if let Some(quit_rx) = tray_quit_rx {
let _ = quit_rx.await;
shutdown_signal.trigger();
} else {
std::future::pending::<()>().await;
}
};
#[cfg(target_os = "windows")]
let server_result = axum::serve(listener, router)
.with_graceful_shutdown(graceful_shutdown)
.await;
#[cfg(not(target_os = "windows"))]
let server_result = axum::serve(listener, router).await;

if let Err(e) = server_result {
tracing::error!("[SERVER] Server error: {}", e);
return ExitCode::from(1);
}
Expand Down
Loading