Compare commits

...
9 Commits
Author SHA1 Message Date
ZacharyZhang-NYandClaude Opus 4.6 c1de05a7f1 chore: bump version to 0.4.97, update build config
Build & Release / Lint & Typecheck (push) Successful in 35s
Build & Release / Unit Tests (push) Successful in 34s
Build & Release / Windows (arm64) (push) Failing after 14s
Build & Release / Windows (x64) (push) Failing after 5m51s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / macOS (x64) (push) Canceled after 0s
Build & Release / Linux (arm64) (push) Canceled after 0s
Build & Release / Linux (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:42:00 -04:00
ZacharyZhang-NYandClaude Opus 4.6 05c57be98f fix: prevent agents from running indefinitely after app quit
Agents kept running after closing AgentCompany because worker processes
auto-recreated on exit, retry timers were never cleared, child CLI
processes weren't terminated, and orchestration had no quit guards.

Changes:
- Add isQuitting guards to all orchestration/automation entry points
- Track and clear all setTimeout/setInterval timers on quit
- Send cancel-run to all active CLI processes before killing worker
- Add destroyed flag to prevent worker recreation after intentional destroy
- Add quit confirmation dialog: Stop All & Quit / Minimize to Background / Cancel
- Bump version to 0.4.96

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 12:40:58 -04:00
ZacharyZhang-NY 222c80e544 更新 README.md 2026-03-25 08:06:13 -04:00
ZacharyZhang-NY 8dd6dd09d6 update 2026-03-25 01:38:49 -04:00
ZacharyZhang-NYandClaude Opus 4.6 eda0e26adb feat: rebrand to website design, Apple HIG icons, ASCII onboarding animation
Build & Release / Lint & Typecheck (push) Successful in 38s
Build & Release / Unit Tests (push) Successful in 31s
Build & Release / Windows (arm64) (push) Failing after 11s
Build & Release / Windows (x64) (push) Failing after 27s
Build & Release / Linux (arm64) (push) Canceled after 0s
Build & Release / Linux (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / macOS (x64) (push) Canceled after 0s
- Align Electron CSS with website design schema (Inter font, #FBFAF5/#191919
  palette, #213448/#94B4C1 accent)
- Generate proper app icons from logo.svg via cairosvg (no more Pillow ladder)
- Square bg icons for Electron/tray (Apple HIG — OS applies squircle mask)
- Circle bg icons for website favicon only
- Add scrolling ASCII agent-activity animation to onboarding hero left panel
- Remove dock.setIcon() — let macOS use bundle .icns with system mask
- Fix goal title overflow with min-w-0 for flex truncation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:39:28 -04:00
ZacharyZhang-NYandClaude Opus 4.6 273244817a feat: rebrand icons — #191919 circle bg, #FBFAF5 logo
Build & Release / Lint & Typecheck (push) Successful in 36s
Build & Release / Unit Tests (push) Successful in 34s
Build & Release / Windows (x64) (push) Failing after 34s
Build & Release / Windows (arm64) (push) Failing after 34s
Build & Release / macOS (x64) (push) Canceled after 0s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / Linux (arm64) (push) Canceled after 50s
Build & Release / Linux (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Replace all orange/red icon assets with new brand colors:
- Circle background #191919, logo mark #FBFAF5
- Regenerated: icon.png, icon.ico, icon.icns, all tray icons
- Updated generate-icons.py with new palette and circle shape
- Added source logo.svg
- Bump version to 0.4.92

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:24:20 -04:00
ZacharyZhang-NYandClaude Opus 4.6 02386999f7 docs: remove local-only tagline, drop Kimi CLI and OpenCode from README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:58:43 -04:00
ZacharyZhang-NYandClaude Opus 4.6 69b25bbda5 docs: add screenshots and redesign README with feature showcase
- Add 7 product screenshots (Dashboard, OrgChart, Messages, Tasks, Approvals, Documents, Onboarding)
- Redesign README with centered logo, badge row, nav links, and feature sections with screenshots
- Add supported AI runtimes table, tech stack table, and architecture overview
- Add landing/ to .gitignore (deployed separately to Cloudflare Pages)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:57:30 -04:00
ZacharyZhang-NYandClaude Opus 4.6 a4fd98d934 fix: add system tray icon for all platforms (macOS, Windows, Linux)
Build & Release / Lint & Typecheck (push) Successful in 1m29s
Build & Release / Unit Tests (push) Successful in 31s
Build & Release / Windows (arm64) (push) Failing after 4m48s
Build & Release / Windows (x64) (push) Failing after 4m42s
Build & Release / Linux (x64) (push) Failing after 19s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / macOS (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Build & Release / Linux (arm64) (push) Failing after 1m9s
Tray was invisible on Windows/Linux (icon not bundled in asar) and
completely absent on macOS (early return guard). Generate dedicated
tray-sized icons, bundle them in the packaged app, and create tray
on all platforms with macOS Template image support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 01:21:17 -04:00
39 changed files with 732 additions and 291 deletions

No files matched your search

+4
View File
@@ -48,6 +48,9 @@ oldreference/
# Local design scratch files
homepagedesign.html
# Landing page (deployed separately to Cloudflare Pages)
landing/
# Package manager logs
npm-debug.log*
pnpm-debug.log*
@@ -57,3 +60,4 @@ CLAUDE.md
build/release/AgentCompany-0.4.61-arm64.dmg
CLAUDE.md
CLAUDE.md
build/release/AgentCompany-0.4.90-arm64.dmg
+146 -12
View File
@@ -1,16 +1,113 @@
# AgentCompany
<p align="center">
<img src="assets/icon.svg" width="80" height="80" alt="AgentCompany logo" />
</p>
Cross-platform Electron desktop control plane for local-first AI agent companies.
<h1 align="center">AgentCompany</h1>
AgentCompany is a standalone desktop application for managing multiple AI coding agents (Codex, Claude Code, Gemini CLI, OpenCode, Kimi Code) as an organization with goals, tasks, approvals, budgets, and audit trails.
<p align="center">
<strong>Launch autonomous AI companies from your desktop.</strong><br/>
Set a goal. Watch AI agents self-organize, delegate, and deliver.<br/>
<strong>PLEASE USE A CODING PLAN, IT COMSUMES A LOT OF TOKENS</strong><br/>
I will add support for Ollama and Github Copilot in the near future.
</p>
<p align="center">
<a href="https://github.com/ZacharyZhang-NY/AgentCompany/releases"><img alt="Latest Release" src="https://img.shields.io/github/v/release/ZacharyZhang-NY/AgentCompany?style=flat-square&color=4a5d7a" /></a>
<a href="https://github.com/ZacharyZhang-NY/AgentCompany/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/ZacharyZhang-NY/AgentCompany?style=flat-square&color=4a5d7a" /></a>
<img alt="Platform" src="https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-4a5d7a?style=flat-square" />
<img alt="Electron" src="https://img.shields.io/badge/electron-39.8.0-4a5d7a?style=flat-square" />
</p>
<p align="center">
<a href="https://github.com/ZacharyZhang-NY/AgentCompany/releases">Download</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="https://agentcompany.pages.dev">Website</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="#features">Features</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a href="#quick-start">Quick Start</a>
</p>
---
<p align="center">
<img src="screenshots/Dashboard.jpeg" width="800" alt="AgentCompany Dashboard — active agents, tasks, and company metrics" />
</p>
## What is AgentCompany?
AgentCompany is a cross-platform desktop app that turns a single goal into a fully autonomous AI company. You provide a company name, description, and mission. A CEO agent self-organizes departments, hires specialists, delegates tasks, and delivers real results — code, content, research, business plans, and more — while you observe through a Slack-style command hub. Inspired by **[Paperclip](https://github.com/paperclipai/paperclip)**
### Supported AI Runtimes
| Runtime | Status |
|---------|--------|
| Claude Code | Ready |
| Codex CLI | Ready |
| Gemini CLI | Ready |
---
## Features
### Self-Organizing Teams
Your AI CEO builds the org chart — recruiting specialists, creating departments, and establishing reporting lines from a single goal.
<p align="center">
<img src="screenshots/Auto-OrgChart-Team.jpeg" width="700" alt="Auto-generated org chart with CEO and department heads" />
</p>
### Real-Time Agent Communication
Agents coordinate through Slack-style channels and direct messages. Watch strategic decisions unfold in real time.
<p align="center">
<img src="screenshots/AI-Internal-Realtime-Message.jpeg" width="700" alt="Real-time messaging between AI agents" />
</p>
### Autonomous Task Management
Goals break down into tasks automatically. Agents pick up work, report progress, and escalate blockers without intervention.
<p align="center">
<img src="screenshots/Auto-Task.jpeg" width="700" alt="Autonomous task queue with goals and status tracking" />
</p>
### Human-in-the-Loop Approvals
Critical decisions surface for your review. Approve, reject, or redirect — your company respects the chain of command.
<p align="center">
<img src="screenshots/Approval-by-Human.jpeg" width="700" alt="Human approval workflow for agent decisions" />
</p>
### AI Document Generation
Agents draft business plans, marketing strategies, and technical specs — stored in a shared document center.
<p align="center">
<img src="screenshots/Document-Center.jpeg" width="700" alt="Document center with AI-generated deliverables" />
</p>
### One-Click Company Creation
Name it, describe the mission, connect a runtime, and launch. Agents start working in under a minute.
<p align="center">
<img src="screenshots/Onboarding.jpeg" width="700" alt="Company creation onboarding flow" />
</p>
---
## Quick Start
### Download
Grab the latest release for your platform:
**[Download AgentCompany](https://github.com/ZacharyZhang-NY/AgentCompany/releases)**
> Available for macOS (Apple Silicon & Intel), Windows, and Linux.
### Build from Source
```sh
# Prerequisites: Node.js 22+, pnpm 10+
pnpm install
pnpm dev
```
---
## Verification
```sh
@@ -26,25 +123,62 @@ pnpm build:verify
## Packaging
```sh
pnpm build # Compile + package the current host platform into /build/release
pnpm build # Compile + package for current platform → /build/release
pnpm build:dir # Compile + unpackaged directory
pnpm build:all # Build for all platforms
```
- Supported targets: macOS, Windows, Linux
- **Targets:** macOS (`.dmg`), Windows (`.exe` / NSIS), Linux (`.AppImage`)
- Packaging uses `electron-builder`
- Local packaging cleans `/build` and injects a non-mutating build version
- Local builds use ad-hoc signing (`CSC_IDENTITY_AUTO_DISCOVERY=false`)
- Defaults to `--publish never`; override with `AGENTCOMPANY_ELECTRON_PUBLISH` env var
- Defaults to `--publish never`; override with `AGENTCOMPANY_ELECTRON_PUBLISH`
---
## Architecture
```
src/
main/ # Electron main process, IPC, orchestration, database
preload/ # Typed bridge (contextIsolation)
renderer/ # React UI
shared/ # Contracts, types shared across processes
main/ # Electron main process — database, services, IPC, orchestration
preload/ # Context-isolated bridge — DesktopApi via contextBridge
renderer/ # React 19 UI — components, hooks, i18n
shared/ # Contracts, types, Zod schemas shared across processes
packages/
adapter-utils/ # Shared adapter utilities
adapter-*/ # Per-connector adapter packages
adapter-claude-local/
adapter-codex-local/
adapter-gemini-local/
```
### Key Design Decisions
- **Local-first:** SQLite database, secrets in OS keychain, no external services required
- **Security:** `contextIsolation: true`, `nodeIntegration: false`, Zod-validated IPC, JWT agent tokens
- **Real autonomy:** Agents communicate, delegate, escalate, and produce deliverables without manual prompting
---
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Framework | Electron 39.8.0 |
| Runtime | Node.js 22+ |
| Language | TypeScript 5.9 (strict) |
| UI | React 19, Tailwind CSS 4, Framer Motion |
| Build | electron-vite, Vite 7, electron-builder |
| Database | SQLite (WAL mode, 25+ tables) |
| Validation | Zod 4 |
| Testing | Vitest, Playwright |
---
## License
[MIT](LICENSE)
---
<p align="center">
<sub>Built with quiet authority. Ship your AI company.</sub>
</p>
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<!-- Apple HIG: full-bleed square, no circle, OS applies mask -->
<rect width="1024" height="1024" fill="#191919"/>
<g transform="translate(146 134) scale(6)">
<path fill="#FBFAF5" d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"/>
<path fill="#FBFAF5" d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 32 KiB

+5 -19
View File
@@ -1,21 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<defs>
<linearGradient id="bg" x1="200" x2="824" y1="96" y2="928" gradientUnits="userSpaceOnUse">
<stop stop-color="#111315"/>
<stop offset="1" stop-color="#1b1f23"/>
</linearGradient>
<radialGradient id="glow" cx="0" cy="0" r="1"
gradientTransform="translate(450 348) rotate(36) scale(800)" gradientUnits="userSpaceOnUse">
<stop stop-color="#d97745" stop-opacity=".13"/>
<stop offset="1" stop-color="#d97745" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="896" height="896" x="64" y="64" rx="224" fill="url(#bg)"/>
<rect width="896" height="896" x="64" y="64" rx="224" fill="url(#glow)"/>
<polygon points="490,200 534,200 578,800 298,800" fill="#d97745" fill-opacity=".94"/>
<polygon points="490,200 534,200 726,800 446,800" fill="#d97745" fill-opacity=".94"/>
<rect x="400" y="419" width="224" height="42" rx="12" fill="#ffebd7"/>
<rect x="374" y="524" width="276" height="42" rx="12" fill="#ebb48c"/>
<rect x="348" y="629" width="328" height="42" rx="12" fill="#be8c69"/>
<circle cx="512" cy="198" r="20" fill="#ffebd7"/>
<circle cx="512" cy="512" r="480" fill="#191919"/>
<g transform="translate(268 255) scale(4)">
<path fill="#FBFAF5" d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"/>
<path fill="#FBFAF5" d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

+1 -1
View File
@@ -4,7 +4,7 @@
"output": "/Users/zacharyzhang/Documents/Github/AgentCompany/build/release"
},
"extraMetadata": {
"version": "0.4.80"
"version": "0.4.95"
},
"mac": {
"hardenedRuntime": false,
+4
View File
@@ -5,6 +5,8 @@ arm64:
- '!build/release{,/**/*}'
- out/**
- package.json
- assets/icon.png
- assets/tray-icon*.png
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
- '!**/._*'
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
@@ -16,3 +18,5 @@ arm64:
- '**/*'
- out/**
- package.json
- assets/icon.png
- assets/tray-icon*.png
+7 -7
View File
@@ -1,8 +1,8 @@
version: 0.4.80
version: 0.4.95
files:
- url: AgentCompany-0.4.80-arm64-mac.zip
sha512: 5gKuP0v6BeMrgiEre/WXXERVAyjh7imWgleoV18ZU5fly+/wq8witKgg8o+E5kid7VUSqTYlvePRtkT8NrYYhg==
size: 113614651
path: AgentCompany-0.4.80-arm64-mac.zip
sha512: 5gKuP0v6BeMrgiEre/WXXERVAyjh7imWgleoV18ZU5fly+/wq8witKgg8o+E5kid7VUSqTYlvePRtkT8NrYYhg==
releaseDate: '2026-03-23T20:47:41.499Z'
- url: AgentCompany-0.4.95-arm64-mac.zip
sha512: /Ew0ls0nISOD/65MHxVbc23TRmgP/bbCWR89j/FhoAwKkE0Vmexjsp6IxY22X8tXnwuMgP+A/DJgqenNlKslzQ==
size: 115753870
path: AgentCompany-0.4.95-arm64-mac.zip
sha512: /Ew0ls0nISOD/65MHxVbc23TRmgP/bbCWR89j/FhoAwKkE0Vmexjsp6IxY22X8tXnwuMgP+A/DJgqenNlKslzQ==
releaseDate: '2026-03-24T20:37:33.571Z'
+2
View File
@@ -6,6 +6,8 @@ directories:
files:
- out/**
- package.json
- assets/icon.png
- assets/tray-icon*.png
asar: true
icon: assets/icon.png
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="122" height="126" viewBox="0 0 122 126" xmlns="http://www.w3.org/2000/svg">
<path id="Path" fill="#FBFAF5" stroke="none" d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"/>
<path id="path1" fill="#FBFAF5" stroke="none" d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-company",
"version": "0.4.90",
"version": "0.4.97",
"private": true,
"type": "module",
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
@@ -35,7 +35,9 @@
"@agentcompany/adapter-codex-local": "workspace:*",
"@agentcompany/adapter-gemini-local": "workspace:*",
"@agentcompany/adapter-utils": "workspace:*",
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "5.2.8",
"@fontsource/instrument-serif": "^5.2.8",
"@phosphor-icons/react": "2.1.10",
"@tanstack/react-virtual": "^3.13.23",
"clsx": "2.1.1",
+16
View File
@@ -20,9 +20,15 @@ importers:
'@agentcompany/adapter-utils':
specifier: workspace:*
version: link:packages/adapter-utils
'@fontsource-variable/inter':
specifier: ^5.2.8
version: 5.2.8
'@fontsource-variable/jetbrains-mono':
specifier: 5.2.8
version: 5.2.8
'@fontsource/instrument-serif':
specifier: ^5.2.8
version: 5.2.8
'@phosphor-icons/react':
specifier: 2.1.10
version: 2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -699,9 +705,15 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@fontsource-variable/inter@5.2.8':
resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
'@fontsource-variable/jetbrains-mono@5.2.8':
resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==}
'@fontsource/instrument-serif@5.2.8':
resolution: {integrity: sha512-s+bkz+syj2rO00Rmq9g0P+PwuLig33DR1xDR8pTWmovH1pUjwnncrFk++q9mmOex8fUQ7oW80gPpPDaw7V1MMw==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -3387,8 +3399,12 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
'@fontsource-variable/inter@5.2.8': {}
'@fontsource-variable/jetbrains-mono@5.2.8': {}
'@fontsource/instrument-serif@5.2.8': {}
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

+144 -163
View File
@@ -1,214 +1,195 @@
#!/usr/bin/env python3
"""Generate AgentCompany app icons (PNG, ICO, ICNS, SVG).
"""Generate AgentCompany icons from SVG sources using cairosvg.
Design v3b: Bold "A" monogram that doubles as an org-chart hierarchy.
Two terracotta legs meet at an apex, with three warm crossbars of
decreasing brightness (CEO → Managers → ICs). All tones stay warm.
Two icon families:
- Web (circle bg): icon.svg → landing favicon + website logo
- App (square bg): icon-app.svg → Electron app icon (Apple HIG, OS applies mask)
- Tray: rendered from logo.svg for menu bar / notification area
"""
from __future__ import annotations
import math
import io
import os
import shutil
import subprocess
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter
# ── Ensure cairo library is discoverable on macOS (Homebrew) ──────────
for _lib_dir in ("/opt/homebrew/opt/cairo/lib", "/usr/local/opt/cairo/lib"):
if Path(_lib_dir).exists():
os.environ["DYLD_LIBRARY_PATH"] = (
_lib_dir + ":" + os.environ.get("DYLD_LIBRARY_PATH", "")
)
break
import cairosvg # noqa: E402 — must come after DYLD_LIBRARY_PATH setup
from PIL import Image # noqa: E402
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "assets"
LANDING = ROOT / "landing" / "public"
ICONSET = ASSETS / "icon.iconset"
PNG_PATH = ASSETS / "icon.png"
ICO_PATH = ASSETS / "icon.ico"
ICNS_PATH = ASSETS / "icon.icns"
SVG_PATH = ASSETS / "icon.svg"
LOGO_SVG = ROOT / "logo.svg"
# SVG sources
WEB_SVG = ASSETS / "icon.svg" # circle bg — web favicon
APP_SVG = ASSETS / "icon-app.svg" # square bg — Electron (Apple HIG)
SIZE = 1024
# ── Brand palette ──────────────────────────────────────────────────────
TERRACOTTA = (217, 119, 69) # #d97745
SLATE_TOP = (17, 19, 21) # #111315
SLATE_BOT = (27, 31, 35) # #1b1f23
# Bar colours — all warm, decreasing brightness
BAR_CEO = (255, 235, 215) # warm cream — brightest
BAR_MGR = (235, 180, 140) # warm peach
BAR_IC = (190, 140, 105) # muted terracotta
# ── Helpers ───────────────────────────────────────────────────────────
def rgba(r: int, g: int, b: int, a: int = 255) -> tuple[int, int, int, int]:
return (r, g, b, a)
def blend(
a: tuple[int, ...], b: tuple[int, ...], t: float,
) -> tuple[int, ...]:
return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(len(a)))
def build_icon() -> Image.Image:
# ── 1. Gradient background with warm glow ──────────────────────────
bg = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
px = bg.load()
top_c = rgba(*SLATE_TOP)
bot_c = rgba(*SLATE_BOT)
glow_cx, glow_cy = SIZE * 0.44, SIZE * 0.34
glow_r = SIZE * 0.78
for y in range(SIZE):
t = y / (SIZE - 1)
base = blend(top_c, bot_c, t)
for x in range(SIZE):
d = math.hypot(x - glow_cx, y - glow_cy) / glow_r
warmth = max(0.0, 1.0 - d) ** 1.8 * 0.13
px[x, y] = blend(base, rgba(*TERRACOTTA), warmth)
# ── 2. Rounded-rect mask + shadow ──────────────────────────────────
icon = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
mask = Image.new("L", (SIZE, SIZE), 0)
inset = 64
ImageDraw.Draw(mask).rounded_rectangle(
(inset, inset, SIZE - inset, SIZE - inset), radius=224, fill=255,
def svg_to_image(svg_path: Path, width: int, height: int | None = None) -> Image.Image:
"""Render an SVG to a Pillow RGBA Image at the given size."""
h = height or width
png_data = cairosvg.svg2png(
url=str(svg_path), output_width=width, output_height=h,
)
shadow_alpha = mask.filter(ImageFilter.GaussianBlur(28))
shadow_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 48))
shadow_layer.putalpha(shadow_alpha)
icon.alpha_composite(shadow_layer, (0, 12))
icon.alpha_composite(bg)
icon.putalpha(mask)
return Image.open(io.BytesIO(png_data)).convert("RGBA")
# ── 3. Draw the "A" mark ───────────────────────────────────────────
overlay = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
cx = SIZE // 2
apex_y = 200
base_y = 800
leg_w = 64
base_half = 250
def leg_x(y: int) -> tuple[float, float]:
t = (y - apex_y) / (base_y - apex_y)
spread = base_half * t
return (cx - spread, cx + spread)
ll_top, _ = leg_x(apex_y)
ll_bot, _ = leg_x(base_y)
_, lr_top = leg_x(apex_y)
_, lr_bot = leg_x(base_y)
left_leg = [
(ll_top - leg_w * 0.35, apex_y),
(ll_top + leg_w * 0.35, apex_y),
(ll_bot + leg_w, base_y),
(ll_bot - leg_w, base_y),
]
right_leg = [
(lr_top - leg_w * 0.35, apex_y),
(lr_top + leg_w * 0.35, apex_y),
(lr_bot + leg_w, base_y),
(lr_bot - leg_w, base_y),
]
draw.polygon(left_leg, fill=rgba(*TERRACOTTA, 240))
draw.polygon(right_leg, fill=rgba(*TERRACOTTA, 240))
# ── 4. Three crossbars — warm tones, hierarchy brightness ──────────
bar_h = 42
bar_r = 12
bar_pad = 14
tiers = [
(440, rgba(*BAR_CEO)),
(545, rgba(*BAR_MGR)),
(650, rgba(*BAR_IC)),
]
for bar_cy, fill in tiers:
left_cx, right_cx = leg_x(bar_cy)
bx0 = int(left_cx + bar_pad)
bx1 = int(right_cx - bar_pad)
draw.rounded_rectangle(
(bx0, bar_cy - bar_h // 2, bx1, bar_cy + bar_h // 2),
radius=bar_r,
fill=fill,
)
# ── 5. Apex beacon ─────────────────────────────────────────────────
dot_r = 20
dot_cy = apex_y - 2
draw.ellipse(
(cx - dot_r, dot_cy - dot_r, cx + dot_r, dot_cy + dot_r),
fill=rgba(*BAR_CEO),
def svg_bytes_to_image(svg_bytes: bytes, width: int, height: int | None = None) -> Image.Image:
"""Render SVG bytes to a Pillow RGBA Image."""
h = height or width
png_data = cairosvg.svg2png(
bytestring=svg_bytes, output_width=width, output_height=h,
)
icon.alpha_composite(overlay)
return icon
return Image.open(io.BytesIO(png_data)).convert("RGBA")
def save_svg() -> None:
svg = """\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<defs>
<linearGradient id="bg" x1="200" x2="824" y1="96" y2="928" gradientUnits="userSpaceOnUse">
<stop stop-color="#111315"/>
<stop offset="1" stop-color="#1b1f23"/>
</linearGradient>
<radialGradient id="glow" cx="0" cy="0" r="1"
gradientTransform="translate(450 348) rotate(36) scale(800)" gradientUnits="userSpaceOnUse">
<stop stop-color="#d97745" stop-opacity=".13"/>
<stop offset="1" stop-color="#d97745" stop-opacity="0"/>
</radialGradient>
</defs>
<rect width="896" height="896" x="64" y="64" rx="224" fill="url(#bg)"/>
<rect width="896" height="896" x="64" y="64" rx="224" fill="url(#glow)"/>
<polygon points="490,200 534,200 578,800 298,800" fill="#d97745" fill-opacity=".94"/>
<polygon points="490,200 534,200 726,800 446,800" fill="#d97745" fill-opacity=".94"/>
<rect x="400" y="419" width="224" height="42" rx="12" fill="#ffebd7"/>
<rect x="374" y="524" width="276" height="42" rx="12" fill="#ebb48c"/>
<rect x="348" y="629" width="328" height="42" rx="12" fill="#be8c69"/>
<circle cx="512" cy="198" r="20" fill="#ffebd7"/>
</svg>
"""
SVG_PATH.write_text(svg, encoding="utf-8")
# ── Web icons (circle background) ────────────────────────────────────
def generate_web_icons() -> None:
"""Generate favicon.ico and icon.png for the landing site (circle bg)."""
print("Generating web icons from icon.svg (circle bg)…")
master = svg_to_image(WEB_SVG, SIZE)
def save_png_and_ico(image: Image.Image) -> None:
ASSETS.mkdir(parents=True, exist_ok=True)
image.save(PNG_PATH, format="PNG")
image.save(
ICO_PATH,
format="ICO",
# PNG for apple-touch-icon / og
web_png = LANDING / "icon.png"
master.save(web_png, format="PNG")
print(f" → {web_png}")
# Copy SVG to landing
shutil.copy2(WEB_SVG, LANDING / "icon.svg")
print(f" → {LANDING / 'icon.svg'}")
# ICO with multiple sizes for favicon
favicon = LANDING / "favicon.ico"
master.save(
favicon, format="ICO",
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
print(f" → {favicon}")
def save_icns(image: Image.Image) -> None:
# ── App icons (square background, Apple HIG) ─────────────────────────
def generate_app_icons() -> None:
"""Generate Electron app icons: PNG, ICO, ICNS (square bg, Apple HIG)."""
print("Generating app icons from icon-app.svg (square bg, Apple HIG)…")
master = svg_to_image(APP_SVG, SIZE)
# Main PNG
png_path = ASSETS / "icon.png"
master.save(png_path, format="PNG")
print(f" → {png_path}")
# Windows ICO
ico_path = ASSETS / "icon.ico"
master.save(
ico_path, format="ICO",
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
print(f" → {ico_path}")
# macOS ICNS via iconutil
generate_icns(master)
def generate_icns(master: Image.Image) -> None:
"""Generate .icns from a master image using macOS iconutil."""
if ICONSET.exists():
shutil.rmtree(ICONSET)
ICONSET.mkdir(parents=True, exist_ok=True)
for size in (16, 32, 64, 128, 256, 512):
image.resize((size, size), Image.Resampling.LANCZOS).save(
master.resize((size, size), Image.Resampling.LANCZOS).save(
ICONSET / f"icon_{size}x{size}.png", format="PNG",
)
image.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save(
master.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save(
ICONSET / f"icon_{size}x{size}@2x.png", format="PNG",
)
icns_path = ASSETS / "icon.icns"
subprocess.run(
["iconutil", "-c", "icns", str(ICONSET), "-o", str(ICNS_PATH)],
["iconutil", "-c", "icns", str(ICONSET), "-o", str(icns_path)],
check=True,
)
shutil.rmtree(ICONSET)
print(f" → {icns_path}")
# ── Tray / notification area icons ───────────────────────────────────
def generate_tray_icons() -> None:
"""Generate tray icons following Apple HIG for menu bar.
- Color (Windows/Linux): square bg with logo, 32×32 and 64×64
- Template (macOS): monochrome black logo on transparent, 22×22 and 44×44
"""
print("Generating tray icons…")
# Color tray icons — render app icon (square bg) at small sizes
for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]:
img = svg_to_image(APP_SVG, size)
img.save(ASSETS / name, format="PNG")
print(f" → {ASSETS / name}")
# macOS template icons — black logo silhouette on transparent
# Read logo.svg paths, render as black on transparent
logo_svg = LOGO_SVG.read_text()
for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]:
# Create SVG with black fill, sized to target
template_svg = _make_template_svg(logo_svg, size)
img = svg_bytes_to_image(template_svg.encode("utf-8"), size)
img.save(ASSETS / name, format="PNG")
print(f" → {ASSETS / name}")
def _make_template_svg(logo_svg_content: str, target_size: int) -> str:
"""Create a macOS template SVG: black logo silhouette on transparent bg."""
# Logo viewBox is 0 0 122 126 — scale to fit target with padding
padding = max(2, int(target_size * 0.1))
available = target_size - 2 * padding
# Scale to fit height (taller dimension)
scale = available / 126
logo_w = 122 * scale
# Center horizontally
offset_x = (target_size - logo_w) / 2
offset_y = padding
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {target_size} {target_size}" fill="none">
<g transform="translate({offset_x:.1f} {offset_y:.1f}) scale({scale:.4f})">
<path fill="#000000" d="M 76.604813 111.661926 C 65.070183 111.662621 53.987053 111.662621 42.757744 111.662621 C 45.2239 101.998177 45.297546 101.949265 53.830105 102.036659 C 57.279823 102.071999 60.730202 102.04248 64.259201 102.04248 C 62.159458 93.316338 61.799103 93.009819 51.340881 91.341682 C 59.312901 73.847687 67.193756 56.553749 75.587578 38.134155 C 86.933441 62.965954 97.804642 86.758881 108.711754 110.630394 C 100.707413 111.871689 92.355965 107.246559 88.107536 98.852119 C 84.207748 91.146538 80.800591 83.191696 77.172813 75.348343 C 76.340195 73.54818 75.492958 71.754791 74.614624 69.877441 C 69.107384 75.691788 68.008286 81.475128 71.005882 88.088753 C 74.413452 95.606903 77.813919 103.128281 81.452705 111.167984 C 79.55114 111.381325 78.303734 111.521271 76.604813 111.661926 Z"/>
<path fill="#000000" d="M 26.805992 87.743256 C 38.616821 61.830872 50.261688 36.260269 62.470131 9.452141 C 65.732315 17.818909 71.145126 23.524567 66.258224 33.086182 C 55.518253 54.099815 46.339973 75.911873 36.533691 97.402336 C 32.699284 105.805443 26.495394 110.823158 16.012421 111.410957 C 19.72117 103.270836 23.180601 95.67794 26.805992 87.743256 Z"/>
</g>
</svg>"""
# ── Main ──────────────────────────────────────────────────────────────
def main() -> None:
ASSETS.mkdir(parents=True, exist_ok=True)
save_svg()
image = build_icon()
save_png_and_ico(image)
save_icns(image)
print(f"Generated: {PNG_PATH}, {ICO_PATH}, {ICNS_PATH}, {SVG_PATH}")
LANDING.mkdir(parents=True, exist_ok=True)
generate_web_icons()
generate_app_icons()
generate_tray_icons()
print("\nDone! All icons generated.")
if __name__ == "__main__":
+22 -1
View File
@@ -69,6 +69,7 @@ interface AutomationDependencies {
notify: (options: NotificationOptions) => void;
wakeAgentIfPossible: (agentId: string | null | undefined, companyId: string, trigger: string) => string | null;
dispatchBrowserAction: (actionId: string, companyId: string) => Promise<void>;
isQuitting: () => boolean;
}
interface RunFinishedEvent {
@@ -86,9 +87,11 @@ export function createAutomationService({
notify,
wakeAgentIfPossible,
dispatchBrowserAction,
isQuitting,
}: AutomationDependencies) {
const activeAutomationExecutions = new Set<string>();
const activeWorkflowTriggerExecutions = new Set<string>();
const pendingScheduledTimers = new Set<ReturnType<typeof setTimeout>>();
function normalizeWorkflowSteps(raw: unknown): WorkflowStepState[] {
if (!Array.isArray(raw)) {
@@ -642,6 +645,7 @@ export function createAutomationService({
}
function dispatchAutomation(trigger: string, ctx: AutomationEventContext) {
if (isQuitting()) return;
evaluateAutomationRules(trigger, ctx);
evaluateWorkflowTriggers(trigger, ctx);
}
@@ -859,6 +863,7 @@ export function createAutomationService({
}
function processRunCompletion(run: RunRecord, event: RunFinishedEvent) {
if (isQuitting()) return;
let task: ReturnType<typeof db.getTask> | null = null;
try {
task = db.getTask(run.taskId);
@@ -1032,6 +1037,7 @@ export function createAutomationService({
}
function handleApprovalResolved(approvalId: string, companyId: string, decision: string) {
if (isQuitting()) return;
const snapshot = db.listSnapshot();
const approval = snapshot.approvals.find((entry) => entry.id === approvalId);
if (!approval) return;
@@ -1168,7 +1174,12 @@ export function createAutomationService({
if (draft.scheduledAt) {
const delay = new Date(draft.scheduledAt).getTime() - Date.now();
if (delay > 0) {
setTimeout(dispatchSocialAction, delay);
const timer = setTimeout(() => {
pendingScheduledTimers.delete(timer);
if (isQuitting()) return;
dispatchSocialAction();
}, delay);
pendingScheduledTimers.add(timer);
} else {
dispatchSocialAction();
}
@@ -1229,6 +1240,7 @@ export function createAutomationService({
}
function handleTaskStatusChange(taskId: string, companyId: string, previousStatus: string, newStatus: string) {
if (isQuitting()) return;
if (previousStatus === newStatus) return;
let task: ReturnType<typeof db.getTask> | null = null;
@@ -1298,6 +1310,7 @@ export function createAutomationService({
}
function handleTaskCreated(taskId: string, companyId: string) {
if (isQuitting()) return;
let task: ReturnType<typeof db.getTask> | null = null;
try {
task = db.getTask(taskId);
@@ -1322,6 +1335,13 @@ export function createAutomationService({
}
}
function destroy() {
for (const timer of pendingScheduledTimers) {
clearTimeout(timer);
}
pendingScheduledTimers.clear();
}
return {
runWorkflowPipeline,
dispatchAutomation,
@@ -1329,5 +1349,6 @@ export function createAutomationService({
handleApprovalResolved,
handleTaskStatusChange,
handleTaskCreated,
destroy,
};
}
+16 -2
View File
@@ -192,6 +192,7 @@ export async function bootstrapApplication({
// Re-register timers for scheduled social posts (survives app restart).
// Scan all companies for queued browser actions with a future scheduledAt time
// and set up setTimeout to dispatch them at the right moment.
const scheduledPostTimers = new Set<ReturnType<typeof setTimeout>>();
try {
const snapshot = db.listSnapshot();
for (const company of snapshot.companies) {
@@ -203,12 +204,15 @@ export async function bootstrapApplication({
if (payload.scheduledAt) {
const delay = new Date(payload.scheduledAt).getTime() - Date.now();
if (delay > 0) {
setTimeout(() => {
const browserActions = serviceGraph.getBrowserActionService();
const timer = setTimeout(() => {
scheduledPostTimers.delete(timer);
if (isQuittingRef.current) return;
const browserActions = serviceGraph?.getBrowserActionService();
if (browserActions) {
void browserActions.dispatchBrowserAction(action.id, company.id);
}
}, delay);
scheduledPostTimers.add(timer);
log.info(`[bootstrap] Re-registered scheduled post timer: action ${action.id} in ${Math.round(delay / 1000)}s`);
}
// If delay <= 0, the action is overdue -- normal dispatch cycle will pick it up
@@ -221,6 +225,16 @@ export async function bootstrapApplication({
log.warn(`[bootstrap] Failed to re-register scheduled post timers: ${error instanceof Error ? error.message : String(error)}`);
}
// Register cleanup for bootstrap-owned resources during quit.
// This runs BEFORE service-graph's onBeforeQuit (which is registered later).
app.on("before-quit", () => {
for (const timer of scheduledPostTimers) {
clearTimeout(timer);
}
scheduledPostTimers.clear();
notificationService.destroy();
});
// Show the window immediately so the user sees the app launch without delay.
await serviceGraph.getDesktopShellService()?.ensureWindow();
serviceGraph.getAppLifecycleService()?.register(hasSingleInstanceLock);
+125 -9
View File
@@ -1,5 +1,5 @@
import { existsSync } from "node:fs";
import { extname } from "node:path";
import { extname, dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { resolve, sep } from "node:path";
import {
@@ -30,6 +30,7 @@ interface DesktopShellDependencies {
onBackupProfile: () => Promise<void>;
onShortcut: (action: "toggle-command-palette" | "toggle-console" | "refresh") => void;
notify: (options: NotificationOptions) => void;
getActiveRunCount: () => number;
}
function isSocialBrowserSession(contents: Electron.WebContents) {
@@ -55,11 +56,91 @@ export function createDesktopShellService({
onBackupProfile,
onShortcut,
notify,
getActiveRunCount,
}: DesktopShellDependencies) {
let mainWindow: BrowserWindow | null = null;
let appTray: Tray | null = null;
let backgroundCloseNoticeShown = false;
let windowCreationPromise: Promise<BrowserWindow | null> | null = null;
let quitDialogShowing = false;
/**
* Quit confirmation gate — shown when agents are actively running.
* Gives the user three choices:
* 0 = "Stop All & Quit" → terminate all agents and exit
* 1 = "Minimize to Background" → hide window, agents keep running
* 2 = "Cancel" → do nothing
*
* When no agents are active the app quits immediately without a dialog.
*/
async function requestQuitWithConfirmation() {
if (quitDialogShowing) return;
let activeRunCount = 0;
try {
activeRunCount = getActiveRunCount();
} catch {
// Cannot determine active runs — proceed with immediate quit as fail-safe
onRequestQuit();
app.quit();
return;
}
if (activeRunCount === 0) {
onRequestQuit();
app.quit();
return;
}
quitDialogShowing = true;
try {
const message = activeRunCount === 1
? "1 agent run is currently active."
: `${activeRunCount} agent runs are currently active.`;
const options: Electron.MessageBoxOptions = {
type: "question",
buttons: ["Stop All & Quit", "Minimize to Background", "Cancel"],
defaultId: 2,
cancelId: 2,
title: "AgentCompany",
message,
detail: "Quitting will stop all running agents and their tasks. You can also minimize to keep agents running in the background.",
};
const parentWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : null;
const result = parentWindow
? await dialog.showMessageBox(parentWindow, options)
: await dialog.showMessageBox(options);
if (result.response === 0) {
// Stop All & Quit
onRequestQuit();
app.quit();
} else if (result.response === 1) {
// Minimize to Background
if (parentWindow) {
parentWindow.hide();
}
if (!backgroundCloseNoticeShown) {
backgroundCloseNoticeShown = true;
notify({
title: "Running in Background",
body: "AgentCompany is still running in the system tray",
urgency: "informational",
navigation: { section: "overview" },
});
}
}
// Cancel: do nothing
} catch {
// Dialog failed (e.g. window destroyed mid-flight) — quit as fail-safe
onRequestQuit();
app.quit();
} finally {
quitDialogShowing = false;
}
}
function getMainWindow() {
return mainWindow;
@@ -195,7 +276,11 @@ export function createDesktopShellService({
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
{
label: `Quit ${app.name}`,
accelerator: "CmdOrCtrl+Q",
click: () => void requestQuitWithConfirmation(),
},
],
};
const template: Electron.MenuItemConstructorOptions[] = [
@@ -222,7 +307,14 @@ export function createDesktopShellService({
await onBackupProfile();
},
},
...(process.platform === "darwin" ? [] : [{ type: "separator" as const }, { role: "quit" as const }]),
...(process.platform === "darwin" ? [] : [
{ type: "separator" as const },
{
label: "Quit",
accelerator: "Alt+F4",
click: () => void requestQuitWithConfirmation(),
},
]),
],
},
{
@@ -275,11 +367,38 @@ export function createDesktopShellService({
}
function createTray() {
if (process.platform === "darwin" || appTray) {
if (appTray) {
return;
}
const trayIcon = nativeImage.createFromPath(iconPath);
const assetsDir = dirname(iconPath);
let trayIcon: Electron.NativeImage;
if (process.platform === "darwin") {
// macOS menu bar: use Template images (monochrome black + alpha).
// Electron auto-detects the @2x variant when the base name includes "Template".
const templatePath = join(assetsDir, "tray-iconTemplate.png");
trayIcon = nativeImage.createFromPath(templatePath);
if (!trayIcon.isEmpty()) {
trayIcon.setTemplateImage(true);
}
} else {
// Windows / Linux: use the color tray icon
const trayPath = join(assetsDir, "tray-icon.png");
trayIcon = nativeImage.createFromPath(trayPath);
}
// Fallback: resize the full app icon
if (trayIcon.isEmpty()) {
const fallback = nativeImage.createFromPath(iconPath);
if (!fallback.isEmpty()) {
trayIcon = fallback.resize({ width: 32, height: 32 });
if (process.platform === "darwin") {
trayIcon.setTemplateImage(true);
}
}
}
appTray = new Tray(trayIcon.isEmpty() ? nativeImage.createEmpty() : trayIcon);
appTray.setToolTip("AgentCompany");
appTray.setContextMenu(
@@ -306,10 +425,7 @@ export function createDesktopShellService({
{ type: "separator" },
{
label: "Quit",
click: () => {
onRequestQuit();
app.quit();
},
click: () => void requestQuitWithConfirmation(),
},
]),
);
+4 -7
View File
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { delimiter as pathDelimiter, join } from "node:path";
import { app, nativeImage } from "electron";
import { app } from "electron";
interface HostEnvironmentDependencies {
iconPath: string;
@@ -91,10 +91,7 @@ export async function prepareHostEnvironment({
logger.info("[path] Skipping interactive shell PATH probe; using deterministic PATH setup only.");
}
const appIcon = nativeImage.createFromPath(iconPath);
if (!appIcon.isEmpty()) {
if (process.platform === "darwin" && app.dock) {
app.dock.setIcon(appIcon);
}
}
// Dock icon: macOS uses the .icns from the app bundle with the system squircle
// mask applied automatically. Do not call dock.setIcon() — it bypasses the mask
// and renders the raw PNG oversized and square.
}
+12 -3
View File
@@ -60,6 +60,7 @@ interface OrchestrationDependencies {
dispatchAutomation: (trigger: string, ctx: AutomationDispatch) => void;
hasWorkerReady: () => boolean;
enqueueWorkerRun: (payload: EnqueueRunPayload) => void;
isQuitting: () => boolean;
}
export function createOrchestrationService({
@@ -72,6 +73,7 @@ export function createOrchestrationService({
dispatchAutomation,
hasWorkerReady,
enqueueWorkerRun,
isQuitting,
}: OrchestrationDependencies) {
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
const scheduleLastFiredMap = new Map<string, number>();
@@ -182,6 +184,9 @@ export function createOrchestrationService({
trigger: string;
}) {
const { runId, companyId, agent, task, connector, workspace, workspaceMode, companyName, companyDescription, snapshot, trigger } = params;
if (isQuitting()) {
throw new Error("Application is shutting down — refusing to dispatch new run.");
}
if (!hasWorkerReady()) {
throw new Error("Orchestration worker is not ready.");
}
@@ -311,6 +316,7 @@ export function createOrchestrationService({
}
function startAgentHeartbeatRun(agentId: string, companyId: string, trigger: string): string | null {
if (isQuitting()) return null;
if (!hasWorkerReady()) return null;
const companyStatus = db.getCompanyStatus(companyId);
if (companyStatus !== "active") {
@@ -448,7 +454,7 @@ export function createOrchestrationService({
}
function wakeAgentIfPossible(agentId: string | null | undefined, companyId: string, trigger: string) {
if (!agentId) return null;
if (!agentId || isQuitting()) return null;
try {
const agent = db.getAgent(agentId);
if (agent.companyId !== companyId) {
@@ -473,6 +479,9 @@ export function createOrchestrationService({
}
function queueTaskRun(taskId: string, trigger: string, actor: string) {
if (isQuitting()) {
return fail("APP_SHUTTING_DOWN", "Application is shutting down.");
}
if (!hasWorkerReady()) {
logger.warn(`[run] Worker not ready — cannot queue run for task ${taskId} (trigger: ${trigger})`);
return fail("WORKER_UNAVAILABLE", "Orchestration worker is not ready.");
@@ -556,7 +565,7 @@ export function createOrchestrationService({
}
function resumeQueuedRuns() {
if (!hasWorkerReady()) {
if (isQuitting() || !hasWorkerReady()) {
return;
}
const snapshot = db.listSnapshot();
@@ -610,7 +619,7 @@ export function createOrchestrationService({
heartbeatTimer = setInterval(() => {
try {
if (!hasWorkerReady()) return;
if (isQuitting() || !hasWorkerReady()) return;
const activeCompanies = db.listActiveCompanyIds();
const now = Date.now();
+11
View File
@@ -135,6 +135,14 @@ export function createServiceGraph({
},
onShortcut: (action) => emitEvent({ type: "shortcut", action }),
notify,
getActiveRunCount: () => {
try {
const snapshot = db.listSnapshot();
return snapshot.runs.filter((r) => r.status === "running" || r.status === "queued").length;
} catch {
return 0;
}
},
});
desktopShellService.initialize();
@@ -169,6 +177,7 @@ export function createServiceGraph({
enqueueWorkerRun: (payload) => {
workerService?.getWorker()?.postMessage({ type: "enqueue-run", payload });
},
isQuitting: () => isQuittingRef.current,
});
workerService.createWorker();
@@ -180,6 +189,7 @@ export function createServiceGraph({
notify,
wakeAgentIfPossible,
dispatchBrowserAction,
isQuitting: () => isQuittingRef.current,
});
onboardingService = createOnboardingService({
@@ -210,6 +220,7 @@ export function createServiceGraph({
onBeforeQuit: () => {
isQuittingRef.current = true;
orchestrationService?.stopHeartbeatScheduler();
automationService?.destroy();
apiServerRef.current?.stop();
apiServerRef.current = null;
browserManagerRef.current?.shutdown();
+33 -6
View File
@@ -83,8 +83,11 @@ export function createWorkerService({
const MAX_CONSECUTIVE_WAKES = 5;
let worker: ReturnType<typeof utilityProcess.fork> | null = null;
let destroyed = false;
const pendingRetryTimers = new Set<ReturnType<typeof setTimeout>>();
function getWorker() {
if (destroyed) return null;
return worker;
}
@@ -98,7 +101,7 @@ export function createWorkerService({
worker.stderr?.on("data", (chunk) => logger.error(`[worker] ${String(chunk).trim()}`));
worker.on("exit", (code) => {
if (isQuitting()) return;
if (isQuitting() || destroyed) return;
logger.warn(`[worker] Worker exited with code ${code}, recovering...`);
@@ -320,7 +323,7 @@ export function createWorkerService({
publishDomainChanged();
}
if (event.status === "failed" || event.status === "timed_out") {
if ((event.status === "failed" || event.status === "timed_out") && !isQuitting() && !destroyed) {
const retryCount = db.getTaskRetryCount(run.taskId);
if (retryCount < MAX_RETRY_ATTEMPTS) {
@@ -329,9 +332,12 @@ export function createWorkerService({
db.advanceTaskStatus(run.taskId, "todo",
`Run ${event.status} (attempt ${retryCount + 1}/${MAX_RETRY_ATTEMPTS}). Auto-retrying after ${RETRY_COOLDOWN_MS / 1000}s cooldown.`);
logger.info(`[escalation] Retrying task ${run.taskId} (attempt ${retryCount + 1}/${MAX_RETRY_ATTEMPTS}) for agent ${run.agentId}`);
setTimeout(() => {
const retryTimer = setTimeout(() => {
pendingRetryTimers.delete(retryTimer);
if (isQuitting() || destroyed) return;
wakeAgentIfPossible(run.agentId, run.companyId, "retry_failed");
}, RETRY_COOLDOWN_MS);
pendingRetryTimers.add(retryTimer);
} else {
// Retry limit reached -- escalate up the chain
db.resetTaskRetryCount(run.taskId);
@@ -376,13 +382,15 @@ export function createWorkerService({
}
const pendingWakes = db.consumePendingWakes(run.agentId);
if (pendingWakes.length > 0) {
if (pendingWakes.length > 0 && !isQuitting() && !destroyed) {
for (const wake of pendingWakes) {
wakeAgentIfPossible(run.agentId, wake.companyId, wake.trigger);
}
}
processRunCompletion(run, event as z.infer<typeof workerMessageSchema> & { type: "run-finished" });
if (!isQuitting() && !destroyed) {
processRunCompletion(run, event as z.infer<typeof workerMessageSchema> & { type: "run-finished" });
}
// Reset retry count on successful completion
if (event.status === "succeeded") {
@@ -396,7 +404,7 @@ export function createWorkerService({
}
// Self-wake with circuit breaker to prevent infinite loops
if (pendingWakes.length === 0) {
if (pendingWakes.length === 0 && !isQuitting() && !destroyed) {
try {
const actionableTasks = db.listAgentTasks(run.agentId, run.companyId, ["todo", "in_progress"]);
if (actionableTasks.length > 0) {
@@ -426,7 +434,26 @@ export function createWorkerService({
}
function destroy() {
destroyed = true;
// Clear all pending retry timers so they never fire after quit
for (const timer of pendingRetryTimers) {
clearTimeout(timer);
}
pendingRetryTimers.clear();
if (worker) {
// Cancel all running/queued runs in the worker before killing it.
// This tells the worker to SIGTERM all child CLI processes (claude, codex, etc.)
const snapshot = db.listSnapshot();
for (const run of snapshot.runs) {
if (run.status === "running" || run.status === "queued") {
try {
worker.postMessage({ type: "cancel-run", runId: run.id });
} catch { /* worker may already be dead */ }
}
}
worker.kill();
// Force-kill after 2s if SIGTERM didn't work (child CLI processes may keep it alive)
const ref = worker;
@@ -0,0 +1,85 @@
import { memo } from "react";
/**
* Scrolling ASCII agent-activity backdrop for the onboarding hero panel.
* Pure CSS animation (translateY on compositor thread) — no JS ticks.
* Content is doubled for seamless looping.
*/
const LINES = [
"",
" ╭──────────╮",
" │ CEO │",
" ╰────┬─────╯",
" ┌────┴─────┐",
" ╭──┴──╮ ╭──┴──╮",
" │ CTO │ │ CMO │",
" ╰──┬──╯ ╰──┬──╯",
" ╭──┴──╮ ╭──┴──╮",
" │ Eng │ │ Mkt │",
" ╰─────╯ ╰─────╯",
"",
" $ deploy --agents 12",
" ████████████████████ 100%",
" [OK] all agents online",
"",
" CEO │ analyzing requirements",
" CEO │ identifying deliverables",
" CEO │ delegating to leads",
"",
" CTO │ reviewing architecture",
" CTO │ → Dev.01: auth module",
" CTO │ → Dev.02: API layer",
" CTO │ → QA: test strategy",
"",
" CMO │ go-to-market strategy",
" CMO │ → Mkt.01: launch copy",
" CMO │ → Mkt.02: social plan",
"",
" ┌─ SPRINT 1 ────────────────┐",
" │ tasks ████████░░ 80% │",
" │ agents 12 active │",
" │ tests 128 passing │",
" └──────────────────────────┘",
"",
" Dev.01 │ scaffolding auth",
" Dev.02 │ REST endpoints",
" Design │ component library",
" QA │ integration tests",
" Mkt.01 │ blog post drafted",
"",
" CEO │ sprint review: ✓",
" CTO │ code review: merged",
" Dev.01 │ PR #12 → main",
" QA │ all tests green",
" CMO │ brief approved",
"",
" [INFO] Sprint 1 complete",
" [INFO] Advancing to Sprint 2",
"",
];
const BLOCK = LINES.join("\n");
export const AsciiAnimation = memo(function AsciiAnimation() {
return (
<div className="pointer-events-none absolute inset-0 select-none overflow-hidden">
{/* Fade masks */}
<div
className="absolute inset-x-0 top-0 z-10 h-32"
style={{ background: "linear-gradient(to bottom, var(--bg), transparent)" }}
/>
<div
className="absolute inset-x-0 bottom-0 z-10 h-32"
style={{ background: "linear-gradient(to top, var(--bg), transparent)" }}
/>
{/* Scrolling track — doubled for seamless loop */}
<div className="ascii-scroll-track whitespace-pre font-mono text-[11px] leading-[1.8] text-[color:var(--accent)] opacity-[0.08]">
{BLOCK}
{"\n"}
{BLOCK}
</div>
</div>
);
});
@@ -8,6 +8,7 @@ import {
import type { ProfileSnapshot } from "@shared/types";
import type { AppLocale } from "@shared/locale";
import { ConnectorReadiness } from "./ConnectorReadiness";
import { AsciiAnimation } from "./AsciiAnimation";
type Step = "language" | "company" | "mission" | "launch";
@@ -57,6 +58,9 @@ export function OnboardingHero({
}}
/>
{/* ASCII agent activity backdrop */}
<AsciiAnimation />
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
@@ -22,12 +22,12 @@ export function GoalProgress({ goals, tasks, onNavigate }: { goals: GoalRecord[]
onClick={() => onNavigate("goals")}
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition hover:border-[color:var(--line-strong)] hover:shadow-sm"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Target size={15} weight="fill" className="text-[color:var(--accent)]" />
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<Target size={15} weight="fill" className="shrink-0 text-[color:var(--accent)]" />
<span className="truncate text-[13px] font-medium text-[color:var(--text)]">{goal.title}</span>
</div>
<span className="whitespace-nowrap text-[13px] font-bold tabular-nums text-[color:var(--accent)]">{pct}%</span>
<span className="shrink-0 whitespace-nowrap text-[13px] font-bold tabular-nums text-[color:var(--accent)]">{pct}%</span>
</div>
<div className="mt-2.5 h-2 w-full overflow-hidden rounded-full bg-[color:var(--panel-soft)]">
<div
@@ -32,10 +32,10 @@ export function GoalList({
onClick={() => onSelect(goal.id)}
className={`cursor-pointer transition-colors duration-100 ${isSelected ? "bg-[color:var(--accent-soft)]" : "hover:bg-[color:var(--panel-soft)]"}`}
>
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
{isSelected && (
<CheckCircle size={16} weight="fill" className="text-[color:var(--accent)]" />
<CheckCircle size={16} weight="fill" className="shrink-0 text-[color:var(--accent)]" />
)}
<div className="truncate text-[14px] font-semibold text-[color:var(--text)]">{goal.title}</div>
</div>
+2
View File
@@ -1,4 +1,6 @@
import "@fontsource-variable/inter";
import "@fontsource-variable/jetbrains-mono";
import "@fontsource/instrument-serif";
import "./styles.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+66 -53
View File
@@ -1,30 +1,32 @@
@import "tailwindcss";
@theme {
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB",
--font-sans: "Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB",
"Noto Sans CJK SC", Helvetica, Arial, sans-serif;
--font-serif: "Instrument Serif", Georgia, "Times New Roman", serif;
--font-mono: "JetBrains Mono Variable", "Microsoft YaHei", ui-monospace,
SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
/* ─── Light theme — aligned with website ─── */
:root {
color-scheme: light;
--bg: #f7f8fa;
--bg-subtle: #ffffff;
--panel: #ffffff;
--panel-soft: #f0f2f5;
--panel-strong: #f7f8fa;
--line: rgba(50, 65, 85, 0.08);
--line-strong: rgba(50, 65, 85, 0.15);
--text: #1a1d24;
--muted: #6b7280;
--muted-strong: #4b5563;
--accent: #4a5d7a;
--accent-text: #3d5070;
--accent-soft: #edf1f7;
--accent-hover: #3d4f6d;
--text-on-accent: #ffffff;
--bg: #FBFAF5;
--bg-subtle: #FFFFFF;
--panel: #FFFFFF;
--panel-soft: #F3F2ED;
--panel-strong: #FBFAF5;
--line: rgba(25, 25, 25, 0.08);
--line-strong: rgba(25, 25, 25, 0.15);
--text: #191919;
--muted: #7a7a72;
--muted-strong: #565650;
--accent: #213448;
--accent-text: #213448;
--accent-soft: rgba(33, 52, 72, 0.07);
--accent-hover: #192838;
--text-on-accent: #FBFAF5;
--danger: #c53030;
--danger-soft: #fef2f2;
--warn: #b45309;
@@ -37,23 +39,24 @@
color-scheme: light;
}
/* ─── Dark theme — aligned with website ─── */
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #16181f;
--bg-subtle: #1a1c24;
--panel: #1e2028;
--panel-soft: #24262e;
--panel-strong: #1c1e26;
--line: rgba(148, 163, 190, 0.12);
--line-strong: rgba(148, 163, 190, 0.2);
--text: #edf0f7;
--muted: #8b95a5;
--muted-strong: #b0b8c8;
--accent: #7b9cc4;
--accent-text: #7b9cc4;
--accent-soft: rgba(123, 156, 196, 0.14);
--accent-hover: #93b0d4;
--text-on-accent: #ffffff;
--bg: #191919;
--bg-subtle: #1e1e1e;
--panel: #222222;
--panel-soft: #2a2a2a;
--panel-strong: #1c1c1c;
--line: rgba(251, 250, 245, 0.08);
--line-strong: rgba(251, 250, 245, 0.15);
--text: #FBFAF5;
--muted: #8a8a84;
--muted-strong: #b8b8b2;
--accent: #94B4C1;
--accent-text: #94B4C1;
--accent-soft: rgba(148, 180, 193, 0.12);
--accent-hover: #a8c4cf;
--text-on-accent: #191919;
--danger: #f87171;
--danger-soft: rgba(248, 113, 113, 0.12);
--warn: #f59e0b;
@@ -65,21 +68,21 @@
@media (prefers-color-scheme: dark) {
:root[data-theme="system"] {
color-scheme: dark;
--bg: #16181f;
--bg-subtle: #1a1c24;
--panel: #1e2028;
--panel-soft: #24262e;
--panel-strong: #1c1e26;
--line: rgba(148, 163, 190, 0.12);
--line-strong: rgba(148, 163, 190, 0.2);
--text: #edf0f7;
--muted: #8b95a5;
--muted-strong: #b0b8c8;
--accent: #7b9cc4;
--accent-text: #7b9cc4;
--accent-soft: rgba(123, 156, 196, 0.14);
--accent-hover: #93b0d4;
--text-on-accent: #ffffff;
--bg: #191919;
--bg-subtle: #1e1e1e;
--panel: #222222;
--panel-soft: #2a2a2a;
--panel-strong: #1c1c1c;
--line: rgba(251, 250, 245, 0.08);
--line-strong: rgba(251, 250, 245, 0.15);
--text: #FBFAF5;
--muted: #8a8a84;
--muted-strong: #b8b8b2;
--accent: #94B4C1;
--accent-text: #94B4C1;
--accent-soft: rgba(148, 180, 193, 0.12);
--accent-hover: #a8c4cf;
--text-on-accent: #191919;
--danger: #f87171;
--danger-soft: rgba(248, 113, 113, 0.12);
--warn: #f59e0b;
@@ -133,11 +136,11 @@ select {
::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(60, 75, 100, 0.2);
background: rgba(25, 25, 25, 0.15);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(60, 75, 100, 0.32);
background: rgba(25, 25, 25, 0.25);
}
.mono {
@@ -147,7 +150,7 @@ select {
.surface {
background: var(--panel);
border: 1px solid var(--line);
box-shadow: 0 1px 3px rgba(26, 29, 36, 0.06), 0 0 0 0.5px rgba(26, 29, 36, 0.03);
box-shadow: 0 1px 3px rgba(25, 25, 25, 0.06), 0 0 0 0.5px rgba(25, 25, 25, 0.03);
}
.focus-ring {
@@ -160,9 +163,19 @@ select {
.focus-ring:focus-visible {
box-shadow:
0 0 0 1px rgba(74, 93, 122, 0.5),
0 0 0 4px rgba(74, 93, 122, 0.12);
border-color: rgba(74, 93, 122, 0.35);
0 0 0 1px rgba(33, 52, 72, 0.5),
0 0 0 4px rgba(33, 52, 72, 0.12);
border-color: rgba(33, 52, 72, 0.35);
}
/* ─── ASCII animation ─── */
@keyframes ascii-scroll-up {
0% { transform: translateY(0); }
100% { transform: translateY(-50%); }
}
.ascii-scroll-track {
animation: ascii-scroll-up 80s linear infinite;
}
@media (prefers-reduced-motion: reduce) {