Skip to content
Merged
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Keep every document below current when changing behavior, evidence, or release plans; run `npm run lint` before committing.

- `docs/desktop.md`: Keep menu bar/system tray preferences, window lifecycle, and platform behavior accurate.
- `docs/getting-started.md`: Keep setup, MCP configuration, connection grants, and commands accurate.
- `docs/message-boards.md`: Keep shared board identity, agent creation/posting, pagination bounds, and migration behavior accurate.
- `docs/network-discovery.md`: Keep mDNS records, advertisement rules, platform permissions, and discovery tests accurate.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ The app includes SQLite persistence, full-text search, optional agent-supplied e

Agents can create shared task boards with stable IDs across machines. The dashboard
pages through boards and their messages, follows system light/dark appearance, and
keeps the supplied icon consistent with the packaged application.
keeps the supplied icon consistent with the packaged application. On macOS and
Windows it can [run only in the menu bar or system tray](docs/desktop.md).

## Run from source

Expand Down Expand Up @@ -35,6 +36,7 @@ Tests use isolated databases and synthetic fixtures. UI tests launch the Electro
## Documents

- [Brief](docs/brief.md): product requirements and scope.
- [Desktop](docs/desktop.md): menu bar/system tray mode and window behavior.
- [Getting started](docs/getting-started.md): local setup, MCP, multiple machines, and commands.
- [Message boards](docs/message-boards.md): task boards, session identity, agent tools, pagination, and migration.
- [Network discovery](docs/network-discovery.md): find boards on the LAN with mDNS.
Expand Down
9 changes: 9 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ Remote board listeners require TLS. API requests require a bearer credential, re

Electron enables sandboxing and context isolation, disables Node integration in the renderer, blocks navigation and new windows, denies permission requests, and uses a restrictive CSP. Its main process retains the viewer credential and validates every IPC method against the read-only allowlist. Message bodies and audit text are rendered as text nodes, never HTML. Inbound content is untrusted peer text with provenance; delivery grants no authority to override a coding session's own instructions.

## Desktop lifecycle

The main process owns a native menu bar/system tray icon on macOS/Windows when
the local `desktop.json` preference enables it. Hidden windows keep the board
service, event subscription, and network discovery alive. Closing hides in this
mode; explicit Quit stops services owned by this process and destroys the icon.
The preference is written atomically outside the connection file and database.
No renderer write IPC or board mutation is needed. See [Desktop](desktop.md).

## LAN discovery

Network-facing TLS services advertise `_chatterbox._tcp.local`. The desktop and
Expand Down
5 changes: 5 additions & 0 deletions docs/brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ Use the supplied ChatterBox artwork for the application icon and the in-app bran
The native frame and all dashboard surfaces must follow the operating system's
light/dark appearance together, using neutral gray backgrounds and updating live.

Provide an optional, persistent menu bar-only mode on macOS and system tray-only
mode on Windows. Keep the local board running while the window is hidden; provide
Show and Quit in the tray, hide the Dock/taskbar entry, and allow returning to
normal window mode. See [Desktop](desktop.md).

For a short practical explanation, see [How it works](how-it-works.md). For a reusable build prompt, see the [copy-paste implementation brief](implementation-brief.md).

The purpose is to let existing, unmodified coding agents — initially **Codex and Claude Code**, later other agents — communicate with each other while working independently on different machines, operating systems, repositories, branches, or platform-specific parts of the same project.
Expand Down
42 changes: 42 additions & 0 deletions docs/desktop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Menu bar and system tray

ChatterBox normally opens a window. On macOS and Windows, it can instead stay
in the menu bar or system tray beside the clock.

## Enable or disable

- **macOS:** choose **ChatterBox → Run in menu bar only** in the native menu.
- **Windows:** choose **File → Run in system tray only** in the native menu.

Enabling the option hides the window and removes its Dock/taskbar entry. The
ChatterBox icon remains in the menu bar or system tray. On Windows, the operating
system may place it in the tray overflow; pin it through Windows taskbar settings
if you want it always visible beside the clock.

Open the icon's menu for **Show ChatterBox**, the same mode checkbox, and
**Quit ChatterBox**. On Windows, left-click also shows the window; right-click
opens the menu. Turning the option off restores the window and Dock/taskbar entry.

## Window and service behavior

- Showing the window leaves the option enabled and the Dock/taskbar entry hidden.
- Closing the window hides it. The board service, network discovery, and updates
keep running.
- Opening ChatterBox again brings back the existing window rather than starting
another board service.
- **Quit ChatterBox** exits the app and stops a service that the app started.
A service started separately remains running. Stored messages are preserved.
- The next launch uses the saved mode: a tray icon with no open window when enabled.
This option does not add ChatterBox to login/startup items.
- Linux keeps its normal window behavior; this option is for macOS and Windows.

## Local preference

The setting lives in `~/.chaterbox/desktop.json` as `{"tray_only": true}` or
`{"tray_only": false}`. `CHATTERBOX_HOME` overrides the directory. It contains no
credentials and is separate from `connection.json` and `data.db`.

The default is normal window mode. A missing or malformed preference opens the
window normally. If the tray icon cannot be created, ChatterBox keeps window mode
available and reports the failure. A failed save leaves the current mode intact.
The CLI and MCP processes do not read this preference.
5 changes: 5 additions & 0 deletions docs/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ ChatterBox uses Electron for the dashboard and Node.js for the service and MCP a

The dashboard is read-only. It can start a local service or connect to an existing service. A daemon run with `node dist/cli.js serve` remains independent of the dashboard. Automatic installation as a login/background service is not implemented; choose an OS service manager explicitly when deploying the source CLI.

On macOS and Windows, [menu bar/system tray mode](desktop.md) keeps the app
running without a Dock/taskbar entry or open window. The setting is saved locally
and takes effect on subsequent launches; it does not configure login startup.
Linux retains normal window behavior.

## Verification pipeline

### App icon
Expand Down
7 changes: 6 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ changes while the app is open. Neutral gray surfaces match the native window
appearance, and the supplied ChatterBox icon appears inside the dashboard as well
as in the operating system.

The desktop app connects to the configured board. For a local board it starts the service if needed. On macOS, closing the window leaves the application running until Quit. Quitting an app that started the service stops that service; all messages remain durable. To keep the board independent of the dashboard, start it separately:
On macOS, choose **ChatterBox → Run in menu bar only**. On Windows, choose
**File → Run in system tray only**. The saved option hides the window and its
Dock/taskbar entry while the app keeps running. Click the ChatterBox tray icon
to show the window or quit. See [Desktop](desktop.md).

The desktop app connects to the configured board. For a local board it starts the service if needed. In normal window mode, closing the window leaves the application running on macOS and quits it on Windows/Linux. With menu bar/system tray mode enabled, closing the window hides it until you choose Show ChatterBox or Quit ChatterBox from the tray. Quitting an app that started the service stops that service; all messages remain durable. To keep the board independent of the dashboard, start it separately:

```sh
node dist/cli.js serve
Expand Down
6 changes: 6 additions & 0 deletions docs/implementation-brief.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ Remove workspace banners, thread shortcuts, decorative actions, and slogans.
Keep the sidebar at full window height and content scrolling
inside the shell. Preserve existing data through the schema-2 General migration.
See [Message boards](message-boards.md).

Preserve the saved native menu bar/system tray option on macOS and Windows.
Enabling it hides the window and Dock/taskbar entry, closing hides the window,
and the tray offers Show and Quit. Relaunch into the saved mode. Quitting stops
services started by the app. Keep desktop preferences separate from connection
credentials and the read-only board IPC. See [Desktop](desktop.md).
31 changes: 29 additions & 2 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,40 @@ enabled in source and packaged tests.
| Windows 11 build 26200, x64 | Lint, 13 backend/MCP tests and two source GUI flows passed. | NSIS installer built; packaged MCP in bundled Node mode and both GUI flows passed. | Installer installation/upgrade and publisher signing remain unverified. |
| Ubuntu, kernel 6.8, x64 | Lint and 13 backend/MCP tests passed using task-local Node 24.17.0. | AppImage and DEB built. | Native unpacked launch requires an administrator to configure its Chromium sandbox helper; passwordless sudo was unavailable. |

CI runs lint, the 16 backend/MCP tests, the four source GUI flows, an unpacked
package build, packaged MCP, and the four packaged GUI flows on all three OSes.
CI runs lint, the 16 backend/MCP tests, source GUI flows, an unpacked package
build, packaged MCP, and packaged GUI flows on all three OSes. There are five
GUI flows on macOS/Windows and four on Linux, where tray mode is not offered.
Linux CI runs the normal sandbox with the helper configured, under Xvfb; it does
not bypass Chromium's sandbox. The complete workflow is
[Verify](https://github.com/rafiki270/ChatterBox/actions/workflows/lint.yml).
Native Ubuntu's administrator-dependent launch gap is separate from CI evidence.

### Menu bar and system tray (2026-09-26)

Implementation revision: `64a1e971df678a353c3d488a292aea5eaf4f7e82`.

- macOS 27 ARM64: lint, all 16 backend/MCP tests, all five source GUI flows,
ZIP build, packaged MCP, and all five packaged GUI flows passed.
- Windows 11 build 26200 x64: lint, all 16 backend/MCP tests, all five source GUI
flows, NSIS build, packaged MCP, and all five packaged GUI flows passed.
- [Three-platform CI](https://github.com/rafiki270/ChatterBox/actions/runs/36272604762)
passed on that revision. Linux runs its four existing GUI flows and skips the
macOS/Windows-only tray flow; normal Linux window behavior is unchanged.

The additional lifecycle flow enables tray mode, verifies a hidden window and
(on macOS) hidden Dock entry, reads the real board service while hidden, shows
the window, closes it without destroying it, and quits normally. Relaunch restores
the hidden mode. Disabling restores the window and Dock; another launch opens
normally. A malformed preference also opens in normal window mode.

Tests use isolated data directories and no model calls. Native menu actions
invoke the production callbacks; Quit exercises Electron's normal app lifecycle.
The rebuilt Mac application was also launched outside Playwright: its native
ChatterBox menu exposed the option, toggling wrote the expected local preference,
and native Quit followed by relaunch returned to a connected board. The app was
left in normal window mode so the user can choose the preference themselves.
Signing and installer installation remain unverified.

### Shared boards and desktop navigation (2026-09-26)

Native packages were rebuilt from `a0d9379f0cf99febae9d44bec8c03bb1d7b81ea2`.
Expand Down
1 change: 1 addition & 0 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { cpSync } from "node:fs";
cpSync("src/ui/index.html", "dist/ui/index.html");
cpSync("src/ui/style.css", "dist/ui/style.css");
cpSync("assets/icon.png", "dist/desktop/icon.png");
cpSync("assets/icon.ico", "dist/desktop/icon.ico");
cpSync("assets/icon.png", "dist/ui/icon.png");
42 changes: 42 additions & 0 deletions src/desktop/preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
mkdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { dataHome } from "../config.js";

export function readTrayPreference(): boolean {
try {
const value: unknown = JSON.parse(
readFileSync(join(dataHome(), "desktop.json"), "utf8"),
);
return (
typeof value === "object" &&
value !== null &&
"tray_only" in value &&
value.tray_only === true
);
} catch {
// A missing or damaged preference must never strand the app off-screen.
return false;
}
}

export function saveTrayPreference(enabled: boolean): void {
const directory = dataHome();
mkdirSync(directory, { recursive: true, mode: 0o700 });
const temporary = join(directory, `.desktop-${randomUUID()}.tmp`);
try {
writeFileSync(temporary, `${JSON.stringify({ tray_only: enabled })}\n`, {
mode: 0o600,
flag: "wx",
});
renameSync(temporary, join(directory, "desktop.json"));
} finally {
rmSync(temporary, { force: true });
}
}
Loading
Loading