diff --git a/assets/icon-app.svg b/assets/icon-app.svg new file mode 100644 index 00000000..9bbff777 --- /dev/null +++ b/assets/icon-app.svg @@ -0,0 +1,8 @@ + diff --git a/assets/icon.icns b/assets/icon.icns index 652b54ee..7c689cac 100644 Binary files a/assets/icon.icns and b/assets/icon.icns differ diff --git a/assets/icon.ico b/assets/icon.ico index 3292508c..336b3b77 100644 Binary files a/assets/icon.ico and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png index 82ee894c..5888660f 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/tray-icon.png b/assets/tray-icon.png index e39da2a0..b0a3876b 100644 Binary files a/assets/tray-icon.png and b/assets/tray-icon.png differ diff --git a/assets/tray-icon@2x.png b/assets/tray-icon@2x.png index 82930d8b..984016a6 100644 Binary files a/assets/tray-icon@2x.png and b/assets/tray-icon@2x.png differ diff --git a/assets/tray-iconTemplate.png b/assets/tray-iconTemplate.png index 6d58b697..774357d9 100644 Binary files a/assets/tray-iconTemplate.png and b/assets/tray-iconTemplate.png differ diff --git a/assets/tray-iconTemplate@2x.png b/assets/tray-iconTemplate@2x.png index 69f50945..df868064 100644 Binary files a/assets/tray-iconTemplate@2x.png and b/assets/tray-iconTemplate@2x.png differ diff --git a/package.json b/package.json index f29fae17..efb3f356 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-company", - "version": "0.4.92", + "version": "0.4.95", "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70cda949..1b8fafe2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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': diff --git a/scripts/generate-icons.py b/scripts/generate-icons.py index d692daaa..87ca1d73 100644 --- a/scripts/generate-icons.py +++ b/scripts/generate-icons.py @@ -1,222 +1,195 @@ #!/usr/bin/env python3 -"""Generate AgentCompany app icons (PNG, ICO, ICNS) and tray icons. +"""Generate AgentCompany icons from SVG sources using cairosvg. -Design v4: Circle background #191919 with #FBFAF5 "A" logo. -Uses the actual logo paths from logo.svg rendered via cairosvg/Pillow. +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 io +import os import shutil import subprocess from pathlib import Path -from PIL import Image, ImageDraw +# ── 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 ────────────────────────────────────────────────────── -BG_COLOR = (25, 25, 25) # #191919 -LOGO_COLOR = (251, 250, 245) # #FBFAF5 +# ── Helpers ─────────────────────────────────────────────────────────── -def build_icon() -> Image.Image: - """Build the app icon by rendering icon.svg via cairosvg, or fallback to Pillow.""" - # Try cairosvg first (best quality) - try: - import cairosvg - svg_data = SVG_PATH.read_bytes() - png_data = cairosvg.svg2png(bytestring=svg_data, output_width=SIZE, output_height=SIZE) - import io - return Image.open(io.BytesIO(png_data)).convert("RGBA") - except Exception: - pass - - # Fallback: draw programmatically with Pillow - icon = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) - draw = ImageDraw.Draw(icon) - - # Circle background - margin = SIZE // 2 - 480 - draw.ellipse( - (margin, margin, SIZE - margin, SIZE - margin), - fill=(*BG_COLOR, 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, ) + return Image.open(io.BytesIO(png_data)).convert("RGBA") - # Draw the "A" monogram — same geometry as the old icon but in new color - cx = SIZE // 2 - apex_y = 200 - base_y = 800 - leg_w = 64 - base_half = 250 - fill = (*LOGO_COLOR, 245) - 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=fill) - draw.polygon(right_leg, fill=fill) - - # Crossbars — all same cream color with varying opacity for hierarchy - bar_h = 42 - bar_r = 12 - bar_pad = 14 - - tiers = [ - (440, (*LOGO_COLOR, 255)), - (545, (*LOGO_COLOR, 210)), - (650, (*LOGO_COLOR, 175)), - ] - - for bar_cy, bar_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=bar_fill, - ) - - # 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=(*LOGO_COLOR, 255), +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, ) - - return icon + return Image.open(io.BytesIO(png_data)).convert("RGBA") -def build_tray_icons() -> None: - """Generate tray/notification area icons for all platforms.""" - # Color tray icon (Windows/Linux) — 32x32 and 64x64 - for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]: - img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) - draw = ImageDraw.Draw(img) - margin = 1 - draw.ellipse((margin, margin, size - margin, size - margin), fill=(*BG_COLOR, 255)) +# ── Web icons (circle background) ──────────────────────────────────── - # Simple A shape scaled to tray size - cx = size // 2 - top_y = int(size * 0.18) - bot_y = int(size * 0.82) - spread = int(size * 0.28) - w = max(2, int(size * 0.06)) +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) - draw.polygon([ - (cx - w, top_y), (cx + w, top_y), - (cx + spread + w, bot_y), (cx + spread - w, bot_y), - ], fill=(*LOGO_COLOR, 240)) - draw.polygon([ - (cx - w, top_y), (cx + w, top_y), - (cx - spread + w, bot_y), (cx - spread - w, bot_y), - ], fill=(*LOGO_COLOR, 240)) + # PNG for apple-touch-icon / og + web_png = LANDING / "icon.png" + master.save(web_png, format="PNG") + print(f" → {web_png}") - # Crossbar - bar_y = int(size * 0.52) - bar_h = max(2, int(size * 0.06)) - bar_left = cx - int(spread * 0.55) - bar_right = cx + int(spread * 0.55) - draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(*LOGO_COLOR, 255)) + # Copy SVG to landing + shutil.copy2(WEB_SVG, LANDING / "icon.svg") + print(f" → {LANDING / 'icon.svg'}") - img.save(ASSETS / name, format="PNG") - - # macOS template icons (monochrome black + alpha) - for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]: - img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) - draw = ImageDraw.Draw(img) - - cx = size // 2 - top_y = int(size * 0.15) - bot_y = int(size * 0.85) - spread = int(size * 0.3) - w = max(1, int(size * 0.06)) - - draw.polygon([ - (cx - w, top_y), (cx + w, top_y), - (cx + spread + w, bot_y), (cx + spread - w, bot_y), - ], fill=(0, 0, 0, 220)) - draw.polygon([ - (cx - w, top_y), (cx + w, top_y), - (cx - spread + w, bot_y), (cx - spread - w, bot_y), - ], fill=(0, 0, 0, 220)) - - bar_y = int(size * 0.52) - bar_h = max(1, int(size * 0.06)) - bar_left = cx - int(spread * 0.55) - bar_right = cx + int(spread * 0.55) - draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(0, 0, 0, 240)) - - img.save(ASSETS / name, format="PNG") - - -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", + # 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"""""" + + +# ── Main ────────────────────────────────────────────────────────────── + def main() -> None: ASSETS.mkdir(parents=True, exist_ok=True) - image = build_icon() - save_png_and_ico(image) - save_icns(image) - build_tray_icons() - print(f"Generated: {PNG_PATH}, {ICO_PATH}, {ICNS_PATH}") - print(f"Generated tray icons in {ASSETS}") + LANDING.mkdir(parents=True, exist_ok=True) + + generate_web_icons() + generate_app_icons() + generate_tray_icons() + + print("\nDone! All icons generated.") if __name__ == "__main__": diff --git a/src/main/host-environment-service.ts b/src/main/host-environment-service.ts index a9968929..2e15e998 100644 --- a/src/main/host-environment-service.ts +++ b/src/main/host-environment-service.ts @@ -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. } diff --git a/src/renderer/components/CompanyOnboarding/AsciiAnimation.tsx b/src/renderer/components/CompanyOnboarding/AsciiAnimation.tsx new file mode 100644 index 00000000..d8fb549a --- /dev/null +++ b/src/renderer/components/CompanyOnboarding/AsciiAnimation.tsx @@ -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 ( +