mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[Haik]: notarized app done and working.
This commit is contained in:
@@ -1,104 +1,242 @@
|
||||
# Open Swarm — Agent Orchestrator
|
||||
<p align="center">
|
||||
<img src="assets/icon.png" alt="Open Swarm" width="128" height="128">
|
||||
</p>
|
||||
|
||||
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.
|
||||
<h1 align="center">Open Swarm</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>An Army of AI Agents at Your Fingertips</strong>
|
||||
<br>
|
||||
A locally-running orchestrator for managing multiple Claude Code instances in parallel.
|
||||
<br>
|
||||
Launch, monitor, and coordinate entire swarms of coding agents from a single interface.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/platform-macOS-lightgrey.svg" alt="Platform"></a>
|
||||
<a href="https://github.com/openswarm-ai/openswarm/stargazers"><img src="https://img.shields.io/github/stars/openswarm-ai/openswarm?style=social" alt="GitHub Stars"></a>
|
||||
<a href="https://github.com/openswarm-ai/openswarm/pulls"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/screenshot.png" alt="Open Swarm Dashboard" width="900">
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
## 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.
|
||||
|
||||
<br>
|
||||
|
||||
## 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.
|
||||
|
||||
<br>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
## 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
|
||||
<br>
|
||||
|
||||
### 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.
|
||||
<br>
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `d` | Go to Dashboard |
|
||||
| `t` | Go to Templates |
|
||||
| `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
|
||||
<br>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
## 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
|
||||
|
||||
<br>
|
||||
|
||||
## 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.
|
||||
|
||||
<br>
|
||||
|
||||
## Community
|
||||
|
||||
- [Twitter / X](https://twitter.com/openswarm_ai)
|
||||
- [Discord](https://discord.gg/openswarm)
|
||||
- [Website](https://openswarm.ai)
|
||||
|
||||
<br>
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE) for details.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 320 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
+12
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Generated
+37
-45
@@ -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",
|
||||
|
||||
+48
-10
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
};
|
||||
@@ -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<Record<string, { x: number; y: number }>>({});
|
||||
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<string>('');
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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<string, { read?: string[]; write?: string[] }> | undefined;
|
||||
const descriptions = (perms._tool_descriptions || {}) as Record<string, string>;
|
||||
@@ -1303,7 +1302,7 @@ const Tools: React.FC = () => {
|
||||
Discover Tools
|
||||
</Button>
|
||||
{!canDiscover && (
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.72rem' }}>Connect the tool first to discover available permissions</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.72rem' }}>Add an MCP configuration to enable tool discovery</Typography>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -455,6 +455,10 @@ const agentsSlice = createSlice({
|
||||
state.expandedSessionIds = [];
|
||||
},
|
||||
|
||||
setExpandedSessionIds(state, action: PayloadAction<string[]>) {
|
||||
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,
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface DashboardLayoutState {
|
||||
cards: Record<string, CardPosition>;
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
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<string, CardPosition>;
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
expandedSessionIds: string[];
|
||||
}
|
||||
|
||||
export const fetchLayout = createAsyncThunk(
|
||||
@@ -71,6 +74,7 @@ export const fetchLayout = createAsyncThunk(
|
||||
cards: (layout.cards ?? {}) as Record<string, CardPosition>,
|
||||
viewCards: (layout.view_cards ?? {}) as Record<string, ViewCardPosition>,
|
||||
browserCards: (layout.browser_cards ?? {}) as Record<string, BrowserCardPosition>,
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+41
-9
@@ -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-<version>.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)
|
||||
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 ""
|
||||
|
||||
Reference in New Issue
Block a user