diff --git a/README.md b/README.md
index 9537c9bc..2bac56bb 100644
--- a/README.md
+++ b/README.md
@@ -1,104 +1,242 @@
-# Open Swarm — Agent Orchestrator
+
+
+
-A locally-running React + FastAPI application for managing multiple Claude Code instances in parallel. Designed for power users who run multiple agents simultaneously and need a unified interface to monitor, control, and coordinate them.
+Open Swarm
+
+
+ An Army of AI Agents at Your Fingertips
+
+ A locally-running orchestrator for managing multiple Claude Code instances in parallel.
+
+ Launch, monitor, and coordinate entire swarms of coding agents from a single interface.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Why Open Swarm?
+
+Running Claude Code in a terminal works fine for one task. But when you're juggling five agents across different branches, approving tool calls in separate windows, and losing track of who's doing what — it falls apart fast.
+
+- **Parallel agents, one screen** — Launch as many Claude Code instances as you need, arranged on a spatial canvas you can pan and zoom freely
+- **Unified approval workflow** — Every tool-use request from every agent surfaces in one place. Approve or deny with a click or a keyboard shortcut.
+- **Full conversation control** — Edit prior messages to fork conversations, navigate between branches, resume closed sessions
+- **100% local** — Everything runs on your machine. No cloud relay, no telemetry, no third-party backend.
+
+
## Features
-- **Multi-agent management** — Launch and monitor multiple Claude Code instances side by side
-- **Git worktree isolation** — Each agent works on its own git worktree/branch to avoid conflicts
-- **Real-time streaming** — WebSocket-based streaming of agent messages and status updates
-- **HITL approvals** — Approve or deny tool usage requests from the dashboard or within each chat
-- **Message branching** — Edit prior messages to fork conversations, navigate between branches
-- **Prompt template library** — Reusable prompt templates with structured input fields, invoked via `/` commands
-- **Skills library** — Manage skills synced to the native `~/.claude/skills/` directory
-- **Tools library** — Define custom tool configurations (bash, MCP, Python)
-- **Keyboard shortcuts** — Navigate between agents and approve/deny requests without a mouse
-- **Diff viewer** — View uncommitted changes in each agent's worktree
+**Spatial Dashboard** — Infinite canvas with drag-and-drop agent cards, view cards, and embedded browser cards. Create multiple dashboards for different workspaces.
+
+**Agent Chat** — Full streaming chat interface powered by WebSockets. Real-time token output, cost tracking per session, and persistent history that survives restarts.
+
+**Human-in-the-Loop Approvals** — Agents request permission before executing tools. Approve or deny individually, or batch-approve from the dashboard. Configurable per-tool permissions (always allow, ask, deny).
+
+**Message Branching** — Edit any prior message to fork the conversation. Navigate freely between branches without losing context.
+
+**Prompt Templates** — Build reusable templates with structured input fields. Invoke them inline via `/` slash commands.
+
+**Skills Library** — Manage skills that sync directly to `~/.claude/skills/`. Browse and install from the official Anthropic skills marketplace.
+
+**Tools Library** — Configure MCP tool servers (stdio, HTTP, SSE) with automatic tool discovery. Browse the MCP registry and Google's catalog with GitHub star counts. Includes Google Workspace OAuth integration.
+
+**Agent Modes** — Five built-in modes (Agent, Ask, Plan, View Builder, Skill Builder) plus custom user-defined modes with configurable system prompts and tool restrictions.
+
+**Views & Outputs** — Create interactive HTML/JS/CSS artifacts rendered in iframes. Supports vibe coding (LLM-generates the view), backend Python execution, auto-run with LLM-generated data, and agent-driven data gathering.
+
+**Git Worktree Isolation** — Each agent operates in its own git worktree and branch, preventing conflicts between parallel workstreams.
+
+**Diff Viewer** — Inspect uncommitted changes in any agent's worktree without leaving the app.
+
+**Cost Tracking** — Real-time USD spend tracking per agent session.
+
+**Dark & Light Themes** — Full theme support with Claude-inspired design tokens.
+
+**Keyboard Shortcuts** — Navigate between agents, approve/deny requests, and switch pages without touching a mouse.
+
+
+
+## Quick Start
+
+### Desktop App
+
+Download the latest release for macOS from [GitHub Releases](https://github.com/openswarm-ai/openswarm/releases).
+
+> Windows and Linux builds are planned but not yet available.
+
+### Development Setup
+
+**Prerequisites:** Python 3.11+, Node.js 18+, Git
+
+```bash
+git clone https://github.com/openswarm-ai/openswarm.git
+cd openswarm
+bash run.sh
+```
+
+This starts the backend (port 8324), frontend (port 3000), and Electron shell together. Once running, set your Anthropic API key in the in-app Settings page.
+
+To run services individually:
+
+```bash
+bash backend/run.sh # API at http://localhost:8324 — docs at /docs
+bash frontend/run.sh # App at http://localhost:3000
+```
+
+
## Architecture
```
-Frontend (React/TypeScript :3000) Backend (FastAPI/Python :8324)
-┌─────────────────────────────┐ ┌──────────────────────────────┐
-│ Dashboard │◄────►│ REST API (/api/*) │
-│ Agent Chat (per session) │ │ WebSocket (/ws/*) │
-│ Templates / Skills / Tools │ │ Agent Manager │
-│ Slash Command Picker │ │ └─ Claude Agent SDK │
-│ Keyboard Shortcuts │ │ Worktree Manager │
-│ Diff Viewer │ │ Library Storage (JSON/files)│
-└─────────────────────────────┘ └──────────────────────────────┘
+Electron Shell (desktop wrapper, auto-updater)
+├─────────────────────────────────────────────────────────────────────┐
+│ │
+│ Frontend (React/TypeScript :3000) Backend (FastAPI :8324) │
+│ ┌───────────────────────────────┐ ┌───────────────────────┐ │
+│ │ Spatial Dashboard Canvas │◄────►│ REST API (/api/*) │ │
+│ │ Agent Chat (streaming) │ │ WebSocket (/ws/*) │ │
+│ │ Templates / Skills / Tools │ WS │ Agent Manager │ │
+│ │ Modes / Views / Commands │◄────►│ └─ claude-agent-sdk│ │
+│ │ Settings │ │ MCP Tool Discovery │ │
+│ │ Redux Toolkit (state) │ │ JSON File Storage │ │
+│ └───────────────────────────────┘ └───────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────┘
```
-## Quick Start
+
-### Prerequisites
+## Configuration
-- Python 3.11+
-- Node.js 18+
-- Git
-- An Anthropic API key (for real agent usage) — `export ANTHROPIC_API_KEY=...`
+The Anthropic API key is configured in-app via the **Settings** page — no environment variable needed for normal usage.
-### Backend
+For advanced configuration, copy `backend/.env.example` to `backend/.env`:
-```bash
-bash backend/run.sh
-# API runs at http://localhost:8324
-# Docs at http://localhost:8324/docs
-```
+| Variable | Purpose |
+|----------|---------|
+| `BACKEND_PORT` | Backend server port (default: `8324`) |
+| `GOOGLE_OAUTH_CLIENT_ID` | Google Workspace integration (Gmail, Calendar, Drive) |
+| `GOOGLE_OAUTH_CLIENT_SECRET` | Google Workspace integration |
+| `APPLE_ID` | macOS code signing & notarization (release builds only) |
+| `APPLE_APP_SPECIFIC_PASSWORD` | macOS notarization (release builds only) |
+| `APPLE_TEAM_ID` | macOS code signing (release builds only) |
+| `GH_TOKEN` | GitHub Releases publishing (release builds only) |
-### Frontend
-
-```bash
-bash frontend/run.sh
-# App runs at http://localhost:3000
-```
-
-### Mock Mode
-
-If `claude-agent-sdk` is not installed, the backend runs in **mock mode** — agents simulate tool calls and responses so you can develop and test the UI without an API key.
+
## Keyboard Shortcuts
| Key | Action |
|-----|--------|
-| `d` | Go to Dashboard |
-| `t` | Go to Templates |
-| `1`–`9` | Open agent by position |
+| `D` | Go to Dashboard |
+| `T` | Go to Templates |
+| `1` – `9` | Open agent by position |
| `Shift+A` | Approve all pending requests |
| `Shift+D` | Deny all pending requests |
| `?` | Show shortcuts help |
-## Slash Commands
+Type `/` in the chat input to invoke prompt templates and skills as slash commands.
-In the chat input, type `/` to invoke templates and skills:
-- `/template-name` — Opens the template's input modal
-- `/skill-name` — Inserts the skill content into the message
+
## Project Structure
```
backend/
apps/
- agents/ — Agent lifecycle, WebSocket, worktree management
- templates/ — Prompt template CRUD (JSON file storage)
- skills/ — Skills CRUD (synced to ~/.claude/skills/)
- tools_lib/ — Tool definitions CRUD (JSON file storage)
- health/ — Health check endpoint
- config/ — FastAPI app configuration
- data/ — Persistent JSON file storage (sessions, dashboards, settings, templates, tools, etc.)
+ agents/ Agent lifecycle, streaming, worktree management
+ dashboards/ Dashboard CRUD and layout persistence
+ dashboard_layout/ Card positions and spatial canvas state
+ templates/ Prompt template CRUD
+ skills/ Skills CRUD (synced to ~/.claude/skills/)
+ tools_lib/ MCP tool configuration and discovery
+ modes/ Agent mode definitions
+ outputs/ Views/outputs, vibe coding, Python executor
+ settings/ App settings and file browser
+ health/ Health check endpoint
+ mcp_registry/ MCP server registry proxy
+ skill_registry/ Anthropic skills marketplace proxy
+ config/ FastAPI app configuration
+ data/ Persistent JSON file storage
frontend/
src/
app/
- components/ — AppShell, NewAgentModal, SlashCommandPicker, KeyboardShortcutsHelp
+ components/ AppShell, Layout, shared UI
pages/
- Dashboard/ — Agent overview grid with live status
- AgentChat/ — Full chat UI with streaming, HITL, branching, diff viewer
- Templates/ — Template library with editor
- Skills/ — Skills library with editor
- Tools/ — Tools library with editor
+ Dashboard/ Spatial canvas with agent/view/browser cards
+ AgentChat/ Streaming chat, HITL approvals, branching, diff viewer
+ Templates/ Template library with structured input fields
+ Skills/ Skills library, skill builder, registry browser
+ Tools/ Tool config, MCP discovery, OAuth, registry browser
+ Modes/ Mode definitions with system prompts
+ Views/ Output artifacts, code editor, vibe coding
+ Commands/ Keyboard shortcuts reference
+ Settings/ App configuration
shared/
- state/ — Redux slices (agents, templates, skills, tools)
- ws/ — WebSocket manager
- hooks/ — Custom hooks (keyboard shortcuts)
+ state/ Redux slices (agents, dashboards, templates, skills, tools, modes, etc.)
+ ws/ WebSocket manager
+ hooks/ Custom hooks
+ styles/ Theme tokens, global styles
+
+electron/
+ main.js Electron main process, auto-updater, Python env management
+ scripts/ Build and notarization scripts
+
+scripts/
+ build-app.sh Desktop app packaging (electron-builder)
+ build-python-env.sh Standalone Python 3.13 environment bundler
```
+
+
+
+## Tech Stack
+
+**Frontend** — React 18, TypeScript, Redux Toolkit, Material UI v7, CodeMirror 6, Framer Motion, React Router v7, Webpack 5
+
+**Backend** — FastAPI, Python 3.11+, Pydantic v2, claude-agent-sdk, Anthropic SDK, WebSockets, httpx
+
+**Desktop** — Electron 33, electron-builder, electron-updater (auto-updates via GitHub Releases)
+
+**Bundled Runtime** — Standalone Python 3.13 (via python-build-standalone) so end users don't need Python installed
+
+
+
+## Contributing
+
+Contributions are welcome. To get started:
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/your-feature`)
+3. Make your changes
+4. Submit a pull request
+
+Please open an issue first for larger changes so we can discuss the approach.
+
+
+
+## Community
+
+- [Twitter / X](https://twitter.com/openswarm_ai)
+- [Discord](https://discord.gg/openswarm)
+- [Website](https://openswarm.ai)
+
+
+
+## License
+
+MIT — see [LICENSE](LICENSE) for details.
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 00000000..8b9f8611
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/screenshot.png b/assets/screenshot.png
new file mode 100644
index 00000000..4448cbe7
Binary files /dev/null and b/assets/screenshot.png differ
diff --git a/backend/.env.example b/backend/.env.example
index 4d293e35..ded015af 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -3,6 +3,17 @@
# =============================================================================
BACKEND_PORT=8324
-
GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=your-google-oauth-client-secret
+
+# =============================================================================
+# macOS Code Signing & Notarization (required for ./scripts/build-app.sh --publish)
+# =============================================================================
+APPLE_ID=your-apple-id@example.com
+APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
+APPLE_TEAM_ID=ABCDE12345
+
+# =============================================================================
+# GitHub Releases (required for --publish)
+# =============================================================================
+GH_TOKEN=ghp_your-github-personal-access-token
diff --git a/backend/ENV_README.md b/backend/ENV_README.md
new file mode 100644
index 00000000..ec04f287
--- /dev/null
+++ b/backend/ENV_README.md
@@ -0,0 +1,227 @@
+# Environment Variables Setup Guide
+
+Copy `.env.example` to `.env` and fill in the values below. This guide walks you through getting every single one.
+
+```bash
+cp .env.example .env
+```
+
+---
+
+## `BACKEND_PORT`
+
+The port the backend server runs on. The default is fine — only change it if something else is already using port 8324.
+
+```
+BACKEND_PORT=8324
+```
+
+---
+
+## `GOOGLE_OAUTH_CLIENT_ID` & `GOOGLE_OAUTH_CLIENT_SECRET`
+
+These let users sign in with their Google account.
+
+### Step 1 — Go to Google Cloud Console
+
+1. Open https://console.cloud.google.com/
+2. Sign in with your Google account (or create one).
+
+### Step 2 — Create a project
+
+1. Click the project dropdown at the very top of the page (it says "Select a project" or shows your current project name).
+2. Click **New Project** in the top-right of the popup.
+3. Name it something like `OpenSwarm`.
+4. Click **Create**.
+5. Wait a few seconds, then click the project dropdown again and select your new `OpenSwarm` project.
+
+### Step 3 — Enable the Google+ API (required for OAuth)
+
+1. In the left sidebar, click **APIs & Services** > **Library**.
+2. Search for `Google+ API` (or `Google Identity`).
+3. Click on it, then click **Enable**.
+
+### Step 4 — Configure the OAuth consent screen
+
+1. In the left sidebar, click **APIs & Services** > **OAuth consent screen**.
+2. Select **External** (unless you're inside a Google Workspace org and only want internal users).
+3. Click **Create**.
+4. Fill in the required fields:
+ - **App name**: `OpenSwarm`
+ - **User support email**: your email
+ - **Developer contact email**: your email
+5. Click **Save and Continue**.
+6. On the **Scopes** page, click **Add or Remove Scopes**.
+ - Check `email` and `profile` (the `openid` scope is added automatically).
+ - Click **Update**, then **Save and Continue**.
+7. On the **Test users** page, click **Add Users**, enter your own email, click **Add**, then **Save and Continue**.
+8. Click **Back to Dashboard**.
+
+### Step 5 — Create OAuth credentials
+
+1. In the left sidebar, click **APIs & Services** > **Credentials**.
+2. Click **+ Create Credentials** at the top.
+3. Select **OAuth client ID**.
+4. For **Application type**, select **Web application**.
+5. **Name**: `OpenSwarm` (or anything you want).
+6. Under **Authorized redirect URIs**, click **+ Add URI** and add:
+ ```
+ http://localhost:8324/api/auth/google/callback
+ ```
+ (Replace `8324` with your `BACKEND_PORT` if you changed it.)
+7. Click **Create**.
+
+### Step 6 — Copy the values
+
+A popup appears with your credentials:
+
+- **Client ID** — copy this into `GOOGLE_OAUTH_CLIENT_ID`
+- **Client Secret** — copy this into `GOOGLE_OAUTH_CLIENT_SECRET`
+
+```
+GOOGLE_OAUTH_CLIENT_ID=123456789-xxxxxxxxx.apps.googleusercontent.com
+GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxx
+```
+
+---
+
+## `APPLE_ID`
+
+This is the email address you use to sign in to your Apple Developer account.
+
+1. If you don't have one, go to https://developer.apple.com/programs/ and click **Enroll**. It costs $99/year.
+2. Once enrolled, your `APPLE_ID` is just the email you signed up with.
+
+```
+APPLE_ID=you@example.com
+```
+
+---
+
+## `APPLE_TEAM_ID`
+
+Your 10-character Apple Developer team identifier.
+
+1. Go to https://developer.apple.com/account
+2. Sign in.
+3. Look at the top-right — your name is shown. Click it, or scroll down.
+4. Under **Membership Details** (or at https://developer.apple.com/account#MembershipDetailsCard), you'll see **Team ID**.
+5. It looks like `ABCDE12345`. Copy it.
+
+```
+APPLE_TEAM_ID=ABCDE12345
+```
+
+---
+
+## `APPLE_APP_SPECIFIC_PASSWORD`
+
+Apple doesn't let you use your regular password for automated tools. You need to generate a special one-time password.
+
+### Step 1 — Turn on two-factor authentication (if you haven't already)
+
+1. On your Mac, go to **System Settings** > **[your name]** > **Sign-In & Security** > **Two-Factor Authentication**.
+2. Turn it on and follow the prompts.
+
+### Step 2 — Generate the app-specific password
+
+1. Go to https://account.apple.com/
+2. Sign in with your Apple ID.
+3. In the **Sign-In and Security** section, click **App-Specific Passwords**.
+4. Click **Generate an app-specific password** (or the **+** button).
+5. Enter a label like `OpenSwarm Notarization`.
+6. Click **Create**.
+7. Apple shows you a password in the format `xxxx-xxxx-xxxx-xxxx`. **Copy it now** — you can't see it again.
+
+```
+APPLE_APP_SPECIFIC_PASSWORD=abcd-efgh-ijkl-mnop
+```
+
+---
+
+## macOS Signing Certificate (no env var, but required)
+
+Before you can sign and notarize, you need a **Developer ID Application** certificate installed in your macOS Keychain. This is what Apple uses to verify that the app was built by you.
+
+### Step 1 — Open Xcode
+
+1. Open **Xcode** on your Mac (install it from the Mac App Store if you don't have it).
+2. Go to **Xcode** menu > **Settings** (or **Preferences** on older versions).
+3. Click the **Accounts** tab.
+4. Click **+** in the bottom-left and sign in with your Apple ID.
+
+### Step 2 — Create the certificate
+
+1. Select your account in the list, then click **Manage Certificates...** in the bottom-right.
+2. Click the **+** in the bottom-left of the popup.
+3. Select **Developer ID Application**.
+4. Click **Create**.
+
+That's it — the certificate is now in your macOS Keychain. `electron-builder` will auto-discover it during builds. You don't need to set any env var for this.
+
+### Alternative — manual method (without Xcode)
+
+1. Go to https://developer.apple.com/account/resources/certificates/list
+2. Click the **+** button.
+3. Select **Developer ID Application**, click **Continue**.
+4. You'll be asked to upload a **Certificate Signing Request (CSR)**:
+ - Open **Keychain Access** on your Mac.
+ - In the menu bar: **Keychain Access** > **Certificate Assistant** > **Request a Certificate From a Certificate Authority**.
+ - Enter your email, leave CA Email blank, select **Saved to disk**, click **Continue**.
+ - Save the `.certSigningRequest` file.
+5. Upload that file on the Apple Developer page, click **Continue**.
+6. Download the `.cer` file.
+7. Double-click it — it installs into your Keychain.
+
+---
+
+## `GH_TOKEN`
+
+A GitHub Personal Access Token that lets the build script upload release artifacts to GitHub Releases.
+
+### Step 1 — Go to GitHub token settings
+
+1. Go to https://github.com/settings/tokens
+2. Sign in if needed.
+
+### Step 2 — Create a token
+
+1. Click **Generate new token** > **Generate new token (classic)**.
+2. **Note**: `OpenSwarm Releases` (or whatever you want).
+3. **Expiration**: pick a duration (90 days, or "No expiration" if you don't want to rotate it).
+4. **Scopes**: check the **`repo`** checkbox (this gives full access to your repositories, which is needed to create releases and upload assets).
+5. Click **Generate token** at the bottom.
+6. **Copy the token now** — it starts with `ghp_` and you won't be able to see it again.
+
+```
+GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+```
+
+---
+
+## Final `.env` example
+
+```
+BACKEND_PORT=8324
+
+GOOGLE_OAUTH_CLIENT_ID=123456789-xxxxxxxxx.apps.googleusercontent.com
+GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxx
+
+APPLE_ID=you@example.com
+APPLE_APP_SPECIFIC_PASSWORD=abcd-efgh-ijkl-mnop
+APPLE_TEAM_ID=ABCDE12345
+
+GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+```
+
+---
+
+## Running a production build
+
+Once your `.env` is filled in, just run:
+
+```bash
+./scripts/build-app.sh --publish
+```
+
+The build script automatically loads `backend/.env`, so you don't need to source it yourself. This will build the app, sign it with your certificate, notarize it with Apple, and upload the `.dmg` and `.zip` to a GitHub Release.
diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py
index 64824afb..9e137e25 100644
--- a/backend/apps/dashboards/dashboards.py
+++ b/backend/apps/dashboards/dashboards.py
@@ -13,6 +13,7 @@ from backend.apps.dashboards.models import (
DashboardLayout,
CardPosition,
ViewCardPosition,
+ BrowserCardPosition,
)
from fastapi import HTTPException
@@ -112,6 +113,7 @@ async def list_dashboards():
items.append({
"id": dumped["id"],
"name": dumped.get("name", "Untitled"),
+ "auto_named": dumped.get("auto_named", False),
"created_at": dumped.get("created_at"),
"updated_at": dumped.get("updated_at"),
})
@@ -125,6 +127,69 @@ async def create_dashboard(body: DashboardCreate):
return dashboard.model_dump(mode="json")
+@dashboards.router.post("/{dashboard_id}/generate-name")
+async def generate_name(dashboard_id: str):
+ dashboard = _load(dashboard_id)
+
+ if not dashboard.auto_named and dashboard.name != "Untitled Dashboard":
+ return {"name": dashboard.name, "auto_named": dashboard.auto_named}
+
+ from backend.apps.agents.agent_manager import agent_manager
+
+ prompts = []
+ for session in agent_manager.sessions.values():
+ if getattr(session, "dashboard_id", None) != dashboard_id:
+ continue
+ for msg in session.messages:
+ if msg.role == "user" and isinstance(msg.content, str) and msg.content.strip():
+ prompts.append(msg.content.strip()[:200])
+ break
+
+ if not prompts:
+ return {"name": dashboard.name, "auto_named": dashboard.auto_named}
+
+ fallback = prompts[0][:40]
+ try:
+ import anthropic
+ from backend.apps.settings.settings import load_settings
+ global_settings = load_settings()
+ if not global_settings.anthropic_api_key:
+ raise ValueError("API key not configured")
+
+ client = anthropic.AsyncAnthropic(api_key=global_settings.anthropic_api_key)
+
+ if len(prompts) == 1:
+ system = (
+ "Generate a concise 2-5 word workspace name for a project based on this task. "
+ "Return only the name, nothing else."
+ )
+ user_content = prompts[0]
+ else:
+ system = (
+ "Generate a concise 2-5 word workspace name that captures the overall theme of these tasks. "
+ "Return only the name, nothing else."
+ )
+ user_content = "\n".join(f"- {p}" for p in prompts)
+
+ resp = await client.messages.create(
+ model="claude-haiku-4-20250414",
+ max_tokens=30,
+ system=system,
+ messages=[{"role": "user", "content": user_content}],
+ )
+ generated = resp.content[0].text.strip().strip('"\'')
+ if generated:
+ fallback = generated
+ except Exception as e:
+ logger.warning(f"Dashboard name generation failed, using fallback: {e}")
+
+ dashboard.name = fallback
+ dashboard.auto_named = True
+ dashboard.updated_at = datetime.now()
+ _save(dashboard)
+ return {"name": dashboard.name, "auto_named": True}
+
+
@dashboards.router.get("/{dashboard_id}")
async def get_dashboard(dashboard_id: str):
dashboard = _load(dashboard_id)
@@ -136,6 +201,7 @@ async def update_dashboard(dashboard_id: str, body: DashboardUpdate):
dashboard = _load(dashboard_id)
if body.name is not None:
dashboard.name = body.name
+ dashboard.auto_named = False
if body.layout is not None:
dashboard.layout = body.layout
dashboard.updated_at = datetime.now()
@@ -188,7 +254,11 @@ async def duplicate_dashboard(dashboard_id: str):
"name": f"{source_data.get('name', 'Untitled')} (copy)",
"created_at": now,
"updated_at": now,
- "layout": {"cards": {}, "view_cards": source_data.get("layout", {}).get("view_cards", {})},
+ "layout": {
+ "cards": {},
+ "view_cards": source_data.get("layout", {}).get("view_cards", {}),
+ "browser_cards": source_data.get("layout", {}).get("browser_cards", {}),
+ },
}
with open(os.path.join(DATA_DIR, f"{new_id}.json"), "w") as f:
json.dump(new_dashboard, f, indent=2)
diff --git a/backend/apps/dashboards/models.py b/backend/apps/dashboards/models.py
index 149f2069..2d5f6d00 100644
--- a/backend/apps/dashboards/models.py
+++ b/backend/apps/dashboards/models.py
@@ -20,14 +20,26 @@ class ViewCardPosition(BaseModel):
height: float = 360
+class BrowserCardPosition(BaseModel):
+ browser_id: str
+ url: str = ""
+ x: float = 0
+ y: float = 0
+ width: float = 640
+ height: float = 480
+
+
class DashboardLayout(BaseModel):
cards: dict[str, CardPosition] = Field(default_factory=dict)
view_cards: dict[str, ViewCardPosition] = Field(default_factory=dict)
+ browser_cards: dict[str, BrowserCardPosition] = Field(default_factory=dict)
+ expanded_session_ids: list[str] = Field(default_factory=list)
class Dashboard(BaseModel):
id: str = Field(default_factory=lambda: uuid4().hex)
name: str = "Untitled Dashboard"
+ auto_named: bool = False
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
layout: DashboardLayout = Field(default_factory=DashboardLayout)
diff --git a/electron/main.js b/electron/main.js
index 1b02b736..7d4168a1 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -11,6 +11,7 @@ let backendPort = null;
const isPackaged = app.isPackaged;
const isDev = process.env.ELECTRON_DEV === '1';
+const iconPath = path.join(__dirname, 'build', 'icon.png');
function getResourcePath(...segments) {
if (isPackaged) {
@@ -118,6 +119,7 @@ function createWindow() {
minWidth: 800,
minHeight: 600,
title: 'OpenSwarm',
+ icon: iconPath,
titleBarStyle: 'hiddenInset',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
@@ -185,6 +187,10 @@ function killBackend() {
}
app.whenReady().then(async () => {
+ if (process.platform === 'darwin' && !isPackaged) {
+ try { app.dock.setIcon(iconPath); } catch (_) {}
+ }
+
try {
if (isDev) {
backendPort = parseInt(process.env.OPENSWARM_PORT || '8324', 10);
diff --git a/electron/package-lock.json b/electron/package-lock.json
index 6453e6d8..5ec26fd8 100644
--- a/electron/package-lock.json
+++ b/electron/package-lock.json
@@ -12,6 +12,7 @@
"get-port": "^5.1.1"
},
"devDependencies": {
+ "@electron/notarize": "^3.1.1",
"electron": "^33.0.0",
"electron-builder": "^25.1.0"
}
@@ -106,57 +107,17 @@
}
},
"node_modules/@electron/notarize": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
- "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz",
+ "integrity": "sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "debug": "^4.1.1",
- "fs-extra": "^9.0.1",
+ "debug": "^4.4.0",
"promise-retry": "^2.0.1"
},
"engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/@electron/notarize/node_modules/fs-extra": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "at-least-node": "^1.0.0",
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@electron/notarize/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/@electron/notarize/node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 10.0.0"
+ "node": ">= 22.12.0"
}
},
"node_modules/@electron/osx-sign": {
@@ -956,6 +917,37 @@
"electron-builder-squirrel-windows": "25.1.8"
}
},
+ "node_modules/app-builder-lib/node_modules/@electron/notarize": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
+ "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.1",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/app-builder-lib/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
diff --git a/electron/package.json b/electron/package.json
index d521a652..3c31d9a1 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -7,6 +7,7 @@
"start": "electron .",
"dev": "ELECTRON_DEV=1 electron .",
"dist": "electron-builder --mac --publish never",
+ "dist:publish": "electron-builder --mac --publish always",
"dist:all": "electron-builder --mac --win --linux"
},
"dependencies": {
@@ -14,6 +15,7 @@
"get-port": "^5.1.1"
},
"devDependencies": {
+ "@electron/notarize": "^3.1.1",
"electron": "^33.0.0",
"electron-builder": "^25.1.0"
},
@@ -27,9 +29,19 @@
"mac": {
"icon": "build/icon.icns",
"target": [
+ {
+ "target": "dmg",
+ "arch": [
+ "arm64",
+ "x64"
+ ]
+ },
{
"target": "zip",
- "arch": ["arm64"]
+ "arch": [
+ "arm64",
+ "x64"
+ ]
}
],
"category": "public.app-category.developer-tools",
@@ -40,37 +52,63 @@
"dmg": {
"title": "OpenSwarm",
"contents": [
- { "x": 130, "y": 220 },
- { "x": 410, "y": 220, "type": "link", "path": "/Applications" }
+ {
+ "x": 130,
+ "y": 220
+ },
+ {
+ "x": 410,
+ "y": 220,
+ "type": "link",
+ "path": "/Applications"
+ }
]
},
"extraResources": [
{
"from": "../frontend/dist",
"to": "frontend",
- "filter": ["**/*"]
+ "filter": [
+ "**/*"
+ ]
},
{
"from": "../backend",
"to": "backend",
- "filter": ["**/*", "!__pycache__/**", "!**/__pycache__/**", "!.venv/**", "!*.pyc"]
+ "filter": [
+ "**/*",
+ "!__pycache__/**",
+ "!**/__pycache__/**",
+ "!.venv/**",
+ "!*.pyc"
+ ]
},
{
"from": "../debugger",
"to": "debugger",
- "filter": ["**/*", "!__pycache__/**", "!**/__pycache__/**", "!*.pyc", "!.venv/**", "!**/node_modules/**"]
+ "filter": [
+ "**/*",
+ "!__pycache__/**",
+ "!**/__pycache__/**",
+ "!*.pyc",
+ "!.venv/**",
+ "!**/.venv/**",
+ "!**/node_modules/**"
+ ]
},
{
"from": "python-env",
"to": "python-env",
- "filter": ["**/*"]
+ "filter": [
+ "**/*"
+ ]
}
],
"publish": {
"provider": "github",
- "owner": "clusterlabs",
- "repo": "openswarm"
+ "owner": "openswarm-ai",
+ "repo": "production"
},
- "afterSign": null
+ "afterSign": "scripts/notarize.js"
}
}
diff --git a/electron/scripts/notarize.js b/electron/scripts/notarize.js
new file mode 100644
index 00000000..76bf1c05
--- /dev/null
+++ b/electron/scripts/notarize.js
@@ -0,0 +1,31 @@
+const { notarize } = require('@electron/notarize');
+
+exports.default = async function notarizing(context) {
+ const { electronPlatformName, appOutDir } = context;
+ if (electronPlatformName !== 'darwin') return;
+
+ if (process.env.CSC_IDENTITY_AUTO_DISCOVERY === 'false') {
+ console.log('Skipping notarization (CSC_IDENTITY_AUTO_DISCOVERY=false)');
+ return;
+ }
+
+ if (!process.env.APPLE_ID || !process.env.APPLE_TEAM_ID) {
+ console.log('Skipping notarization (APPLE_ID or APPLE_TEAM_ID not set)');
+ return;
+ }
+
+ const appName = context.packager.appInfo.productFilename;
+ const appPath = `${appOutDir}/${appName}.app`;
+
+ console.log(`Notarizing ${appPath}...`);
+
+ await notarize({
+ appBundleId: 'com.clusterlabs.openswarm',
+ appPath,
+ appleId: process.env.APPLE_ID,
+ appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
+ teamId: process.env.APPLE_TEAM_ID,
+ });
+
+ console.log('Notarization complete.');
+};
diff --git a/frontend/src/app/pages/Dashboard/Dashboard.tsx b/frontend/src/app/pages/Dashboard/Dashboard.tsx
index 7a278d18..4b44d41f 100644
--- a/frontend/src/app/pages/Dashboard/Dashboard.tsx
+++ b/frontend/src/app/pages/Dashboard/Dashboard.tsx
@@ -13,6 +13,7 @@ import {
launchAndSendFirstMessage,
generateTitle,
resumeSession,
+ setExpandedSessionIds,
} from '@/shared/state/agentsSlice';
import type { AgentConfig } from '@/shared/state/agentsSlice';
import {
@@ -26,6 +27,7 @@ import {
resetLayout,
} from '@/shared/state/dashboardLayoutSlice';
import { fetchOutputs } from '@/shared/state/outputsSlice';
+import { generateDashboardName } from '@/shared/state/dashboardsSlice';
import { dashboardWs } from '@/shared/ws/WebSocketManager';
import AgentCard from './AgentCard';
import DashboardViewCard from './DashboardViewCard';
@@ -70,6 +72,7 @@ const DashboardInner: React.FC = () => {
const viewCards = useAppSelector((state) => state.dashboardLayout.viewCards);
const browserCards = useAppSelector((state) => state.dashboardLayout.browserCards);
const layoutInitialized = useAppSelector((state) => state.dashboardLayout.initialized);
+ const persistedExpandedSessionIds = useAppSelector((state) => state.dashboardLayout.persistedExpandedSessionIds);
const zoomSensitivity = useAppSelector((state) => state.settings.data.zoom_sensitivity);
const newAgentShortcut = useAppSelector((state) => state.settings.data.new_agent_shortcut);
const browserHomepage = useAppSelector((state) => state.settings.data.browser_homepage);
@@ -88,6 +91,7 @@ const DashboardInner: React.FC = () => {
const [toolbarOpen, setToolbarOpen] = useState(false);
const spawnOriginsRef = useRef>({});
const hasFittedRef = useRef(false);
+ const restoredExpandedRef = useRef(false);
// ---- Multi-drag coordination ----
const [multiDragDelta, setMultiDragDelta] = useState<{ dx: number; dy: number } | null>(null);
@@ -157,6 +161,7 @@ const DashboardInner: React.FC = () => {
useEffect(() => {
if (!dashboardId) return;
hasFittedRef.current = false;
+ restoredExpandedRef.current = false;
dispatch(resetLayout());
dispatch(fetchSessions({ dashboardId }));
dispatch(fetchHistory({ dashboardId }));
@@ -173,6 +178,14 @@ const DashboardInner: React.FC = () => {
return () => clearTimeout(timer);
}, [layoutInitialized, canvas.actions]);
+ useEffect(() => {
+ if (!layoutInitialized || restoredExpandedRef.current) return;
+ restoredExpandedRef.current = true;
+ if (persistedExpandedSessionIds.length > 0) {
+ dispatch(setExpandedSessionIds(persistedExpandedSessionIds));
+ }
+ }, [layoutInitialized, persistedExpandedSessionIds, dispatch]);
+
const prevSessionIdsRef = useRef('');
useEffect(() => {
@@ -189,6 +202,7 @@ const DashboardInner: React.FC = () => {
const cardsJson = JSON.stringify(cards);
const viewCardsJson = JSON.stringify(viewCards);
const browserCardsJson = JSON.stringify(browserCards);
+ const expandedJson = JSON.stringify(expandedSessionIds);
const skipInitialSave = useRef(true);
useEffect(() => {
if (!layoutInitialized || !dashboardId) return;
@@ -196,8 +210,8 @@ const DashboardInner: React.FC = () => {
skipInitialSave.current = false;
return;
}
- dispatch(saveLayout({ dashboardId, cards, viewCards, browserCards }));
- }, [cardsJson, viewCardsJson, browserCardsJson, layoutInitialized, dashboardId]);
+ dispatch(saveLayout({ dashboardId, cards, viewCards, browserCards, expandedSessionIds }));
+ }, [cardsJson, viewCardsJson, browserCardsJson, expandedJson, layoutInitialized, dashboardId]);
useEffect(() => {
const parts = newAgentShortcut.toLowerCase().split('+');
@@ -276,6 +290,22 @@ const DashboardInner: React.FC = () => {
dispatch(generateTitle({ sessionId: realId, prompt }));
spawnOriginsRef.current[realId] = spawnOriginsRef.current[draftId];
delete spawnOriginsRef.current[draftId];
+
+ if (dashboardId) {
+ const currentSessions = store.getState().agents.sessions;
+ const agentCount = Object.values(currentSessions).filter(
+ (s) => s.status !== 'draft' && s.dashboard_id === dashboardId,
+ ).length;
+ const NAME_GEN_TRIGGERS = [1, 3, 6];
+ const currentDash = store.getState().dashboards.items[dashboardId];
+ const canAutoName =
+ currentDash &&
+ (currentDash.auto_named || currentDash.name === 'Untitled Dashboard');
+
+ if (NAME_GEN_TRIGGERS.includes(agentCount) && canAutoName) {
+ dispatch(generateDashboardName(dashboardId));
+ }
+ }
} else {
delete spawnOriginsRef.current[draftId];
}
diff --git a/frontend/src/app/pages/Tools/Tools.tsx b/frontend/src/app/pages/Tools/Tools.tsx
index c04eb01f..be5d842f 100644
--- a/frontend/src/app/pages/Tools/Tools.tsx
+++ b/frontend/src/app/pages/Tools/Tools.tsx
@@ -681,8 +681,7 @@ const Tools: React.FC = () => {
try { parsedConfig = JSON.parse(mcpConfigJson); } catch { setMcpConfigError('Invalid JSON'); return; }
const f = serverToToolForm(mcpConfigServer);
- const isStdioConfig = parsedConfig.type === 'stdio' || !!parsedConfig.command;
- const authStatus = (mcpAuthType !== 'none' || isStdioConfig) ? 'configured' : 'none';
+ const authStatus = 'configured';
await dispatch(createTool({
name: f.name,
@@ -1038,7 +1037,7 @@ const Tools: React.FC = () => {
const isExpanded = expandedToolId === tool.id;
const isMcp = tool.mcp_config && Object.keys(tool.mcp_config).length > 0;
const isStdio = isMcp && (tool.mcp_config.type === 'stdio' || !!tool.mcp_config.command);
- const canDiscover = isMcp && (isStdio || tool.auth_status !== 'none');
+ const canDiscover = isMcp;
const perms = tool.tool_permissions || {};
const services = perms._services as Record | undefined;
const descriptions = (perms._tool_descriptions || {}) as Record;
@@ -1303,7 +1302,7 @@ const Tools: React.FC = () => {
Discover Tools
{!canDiscover && (
- Connect the tool first to discover available permissions
+ Add an MCP configuration to enable tool discovery
)}
) : (
diff --git a/frontend/src/shared/state/agentsSlice.ts b/frontend/src/shared/state/agentsSlice.ts
index 0969df04..2186a419 100644
--- a/frontend/src/shared/state/agentsSlice.ts
+++ b/frontend/src/shared/state/agentsSlice.ts
@@ -455,6 +455,10 @@ const agentsSlice = createSlice({
state.expandedSessionIds = [];
},
+ setExpandedSessionIds(state, action: PayloadAction) {
+ state.expandedSessionIds = action.payload;
+ },
+
updateSessionName(state, action: PayloadAction<{ sessionId: string; name: string }>) {
const session = state.sessions[action.payload.sessionId];
if (session) {
@@ -839,6 +843,7 @@ export const {
expandSession,
collapseSession,
collapseAllSessions,
+ setExpandedSessionIds,
updateSessionName,
updateGroupMeta,
setDraftSystemPrompt,
diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts
index e7540286..3c458517 100644
--- a/frontend/src/shared/state/dashboardLayoutSlice.ts
+++ b/frontend/src/shared/state/dashboardLayoutSlice.ts
@@ -43,6 +43,7 @@ export interface DashboardLayoutState {
cards: Record;
viewCards: Record;
browserCards: Record;
+ persistedExpandedSessionIds: string[];
loading: boolean;
initialized: boolean;
}
@@ -51,6 +52,7 @@ const initialState: DashboardLayoutState = {
cards: {},
viewCards: {},
browserCards: {},
+ persistedExpandedSessionIds: [],
loading: false,
initialized: false,
};
@@ -59,6 +61,7 @@ interface LayoutPayload {
cards: Record;
viewCards: Record;
browserCards: Record;
+ expandedSessionIds: string[];
}
export const fetchLayout = createAsyncThunk(
@@ -71,6 +74,7 @@ export const fetchLayout = createAsyncThunk(
cards: (layout.cards ?? {}) as Record,
viewCards: (layout.view_cards ?? {}) as Record,
browserCards: (layout.browser_cards ?? {}) as Record,
+ expandedSessionIds: (layout.expanded_session_ids ?? []) as string[],
} satisfies LayoutPayload;
},
);
@@ -91,7 +95,12 @@ export const saveLayout = createAsyncThunk(
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- layout: { cards: payload.cards, view_cards: payload.viewCards, browser_cards: payload.browserCards },
+ layout: {
+ cards: payload.cards,
+ view_cards: payload.viewCards,
+ browser_cards: payload.browserCards,
+ expanded_session_ids: payload.expandedSessionIds,
+ },
}),
});
resolve(payload);
@@ -379,6 +388,7 @@ const dashboardLayoutSlice = createSlice({
state.cards = {};
state.viewCards = {};
state.browserCards = {};
+ state.persistedExpandedSessionIds = [];
state.initialized = false;
},
@@ -394,6 +404,7 @@ const dashboardLayoutSlice = createSlice({
state.cards = action.payload.cards;
state.viewCards = action.payload.viewCards;
state.browserCards = action.payload.browserCards;
+ state.persistedExpandedSessionIds = action.payload.expandedSessionIds;
})
.addCase(fetchLayout.rejected, (state) => {
state.loading = false;
diff --git a/frontend/src/shared/state/dashboardsSlice.ts b/frontend/src/shared/state/dashboardsSlice.ts
index 5f8bb4fa..16d31de8 100644
--- a/frontend/src/shared/state/dashboardsSlice.ts
+++ b/frontend/src/shared/state/dashboardsSlice.ts
@@ -6,6 +6,7 @@ const DASHBOARDS_API = `${API_BASE}/dashboards`;
export interface Dashboard {
id: string;
name: string;
+ auto_named: boolean;
created_at: string;
updated_at: string;
}
@@ -66,6 +67,17 @@ export const duplicateDashboard = createAsyncThunk(
},
);
+export const generateDashboardName = createAsyncThunk(
+ 'dashboards/generateName',
+ async (dashboardId: string) => {
+ const res = await fetch(`${DASHBOARDS_API}/${dashboardId}/generate-name`, {
+ method: 'POST',
+ });
+ const data = await res.json();
+ return { id: dashboardId, name: data.name as string, auto_named: data.auto_named as boolean };
+ },
+);
+
const dashboardsSlice = createSlice({
name: 'dashboards',
initialState,
@@ -92,7 +104,12 @@ const dashboardsSlice = createSlice({
.addCase(renameDashboard.fulfilled, (state, action) => {
const d = action.payload;
if (state.items[d.id]) {
- state.items[d.id] = { ...state.items[d.id], name: d.name, updated_at: d.updated_at };
+ state.items[d.id] = {
+ ...state.items[d.id],
+ name: d.name,
+ auto_named: d.auto_named ?? false,
+ updated_at: d.updated_at,
+ };
}
})
.addCase(deleteDashboard.fulfilled, (state, action) => {
@@ -100,6 +117,13 @@ const dashboardsSlice = createSlice({
})
.addCase(duplicateDashboard.fulfilled, (state, action) => {
state.items[action.payload.id] = action.payload;
+ })
+ .addCase(generateDashboardName.fulfilled, (state, action) => {
+ const { id, name, auto_named } = action.payload;
+ if (state.items[id]) {
+ state.items[id].name = name;
+ state.items[id].auto_named = auto_named;
+ }
});
},
});
diff --git a/scripts/build-app.sh b/scripts/build-app.sh
index 6a8d0a50..4453332d 100755
--- a/scripts/build-app.sh
+++ b/scripts/build-app.sh
@@ -3,21 +3,50 @@ set -euo pipefail
# Master build script for the OpenSwarm desktop app.
#
-# Steps:
-# 1. Build the React frontend (webpack production build)
-# 2. Set up the embedded Python environment
-# 3. Package everything with electron-builder
-#
-# Output: electron/dist/OpenSwarm-.dmg
+# Usage:
+# bash scripts/build-app.sh Local dev build (unsigned)
+# bash scripts/build-app.sh --publish Production build (signed, notarized, published to GitHub Releases)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
+ENV_FILE="$PROJECT_ROOT/backend/.env"
+if [[ -f "$ENV_FILE" ]]; then
+ set -a
+ source "$ENV_FILE"
+ set +a
+fi
+
+PUBLISH_MODE=false
+if [[ "${1:-}" == "--publish" ]]; then
+ PUBLISH_MODE=true
+fi
+
echo "========================================"
echo " OpenSwarm Desktop App Builder"
+if $PUBLISH_MODE; then
+ echo " Mode: PRODUCTION (sign + notarize + publish)"
+else
+ echo " Mode: LOCAL (unsigned)"
+fi
echo "========================================"
echo ""
+if $PUBLISH_MODE; then
+ missing_vars=()
+ [[ -z "${APPLE_ID:-}" ]] && missing_vars+=("APPLE_ID")
+ [[ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]] && missing_vars+=("APPLE_APP_SPECIFIC_PASSWORD")
+ [[ -z "${APPLE_TEAM_ID:-}" ]] && missing_vars+=("APPLE_TEAM_ID")
+ [[ -z "${GH_TOKEN:-}" ]] && missing_vars+=("GH_TOKEN")
+ if [[ ${#missing_vars[@]} -gt 0 ]]; then
+ echo "ERROR: Missing required environment variables for --publish mode:"
+ printf ' - %s\n' "${missing_vars[@]}"
+ echo ""
+ echo "See script header for details."
+ exit 1
+ fi
+fi
+
# Step 1: Build frontend
echo "[1/3] Building frontend..."
cd "$PROJECT_ROOT/frontend"
@@ -47,10 +76,12 @@ echo "[3/3] Packaging with electron-builder..."
cd "$PROJECT_ROOT/electron"
npm install
-# Skip code signing for local builds (set CSC_LINK for production signing)
-export CSC_IDENTITY_AUTO_DISCOVERY=false
-
-npx electron-builder --mac --publish never
+if $PUBLISH_MODE; then
+ npx electron-builder --mac --publish always
+else
+ export CSC_IDENTITY_AUTO_DISCOVERY=false
+ npx electron-builder --mac --publish never
+fi
echo ""
echo "========================================"
@@ -58,5 +89,6 @@ echo " Build Complete!"
echo "========================================"
echo ""
echo "Output files:"
-ls -lh "$PROJECT_ROOT/electron/dist/"*.zip 2>/dev/null || echo " (no .zip found — check electron/dist/)"
+ls -lh "$PROJECT_ROOT/electron/dist/"*.dmg 2>/dev/null || true
+ls -lh "$PROJECT_ROOT/electron/dist/"*.zip 2>/dev/null || true
echo ""