From 5839fae786dae581379923e112cb0e93156308b4 Mon Sep 17 00:00:00 2001 From: yyovil Date: Wed, 10 Jun 2026 08:02:39 +0530 Subject: [PATCH 1/2] chore: add prettier config and CI auto-formatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .prettierrc and .prettierignore (config only, no local enforcement). Formatting runs in CI via the prettier.yml workflow: on every push to a non-main branch, Prettier rewrites changed files and commits the result back using GITHUB_TOKEN. Developers never need to run Prettier locally. Intentionally excludes husky/lint-staged — local pre-commit hooks are the wrong layer for a formatter that the whole team doesn't need installed. Also adds .envrc.local to .gitignore for personal local shell overrides. Co-Authored-By: Claude Sonnet 4.6 Entire-Checkpoint: 27336650d2ee --- .github/workflows/prettier.yml | 38 ++++++++++++++++++++++++++++++++++ .gitignore | 3 +++ .prettierignore | 17 +++++++++++++++ .prettierrc | 9 ++++++++ 4 files changed, 67 insertions(+) create mode 100644 .github/workflows/prettier.yml create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 00000000..b9a10b8d --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,38 @@ +name: Prettier + +# Auto-formats the codebase on every push and commits the result back. +# Formatting is a CI concern — developers never need to run Prettier locally +# and formatted output never shows up as local uncommitted changes. +# +# GitHub Actions does not re-trigger workflows on commits made with GITHUB_TOKEN, +# so there is no feedback loop risk. + +on: + push: + branches-ignore: + - main + - "entire/**" + - "worktree-**" + +jobs: + format: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Format with Prettier + run: npx --yes prettier@3 --write . + + - name: Commit formatted files + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git diff --quiet && exit 0 + git add -A + git commit -m "chore: format with prettier [skip ci]" + git push diff --git a/.gitignore b/.gitignore index 85b14354..74f9d354 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ session-events.jsonl.* # OS .DS_Store Thumbs.db + +# Personal local overrides (not for the team) +.envrc.local diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..4be88241 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,17 @@ +# Generated — never hand-edit; regenerated by `npm run api` / sqlc / openapi-typescript +frontend/src/api/schema.ts +backend/internal/httpd/apispec/openapi.yaml + +# Build outputs +frontend/dist +frontend/dist-electron +frontend/release +frontend/test-results +frontend/playwright-report + +# Lockfiles +package-lock.json +frontend/package-lock.json + +# Go uses gofmt, not Prettier +backend/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..2b07565c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "tabWidth": 2, + "printWidth": 120, + "singleQuote": false, + "trailingComma": "all", + "semi": true, + "arrowParens": "always" +} From a488531d074a19217c910e0849ba9c842f1f594e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jun 2026 02:33:08 +0000 Subject: [PATCH 2/2] chore: format with prettier [skip ci] --- AGENTS.md | 4 + README.md | 68 +- docs/README.md | 12 +- docs/agent/README.md | 1 - docs/cli/README.md | 36 +- docs/design/per-project-config.md | 38 +- docs/stack.md | 60 +- frontend/package.json | 28 +- frontend/src/landing/app/docs/404/page.tsx | 14 +- .../src/landing/app/docs/[[...slug]]/page.tsx | 127 ++- frontend/src/landing/app/docs/docs.css | 495 +++++---- frontend/src/landing/app/docs/layout.tsx | 192 ++-- frontend/src/landing/app/docs/not-found.tsx | 2 +- frontend/src/landing/app/landing/layout.tsx | 50 +- frontend/src/landing/app/landing/page.tsx | 54 +- frontend/src/landing/app/layout.tsx | 14 +- frontend/src/landing/app/page.tsx | 12 +- .../src/landing/components/LandingAbout.tsx | 106 +- .../landing/components/LandingAgentsBar.tsx | 86 +- .../src/landing/components/LandingCTA.tsx | 62 +- .../components/LandingDifferentiators.tsx | 112 +-- .../landing/components/LandingFeatures.tsx | 946 +++++++++--------- .../src/landing/components/LandingHero.tsx | 191 ++-- .../landing/components/LandingHowItWorks.tsx | 569 +++++------ .../src/landing/components/LandingNav.tsx | 153 +-- .../landing/components/LandingQuickStart.tsx | 86 +- .../src/landing/components/LandingStats.tsx | 81 +- .../components/LandingTestimonials.tsx | 289 +++--- .../landing/components/LandingUseCases.tsx | 405 ++++---- .../src/landing/components/LandingVideo.tsx | 36 +- .../landing/components/LandingWorkflow.tsx | 589 +++++------ .../landing/components/PageConstellation.tsx | 293 +++--- .../components/ScrollRevealProvider.tsx | 34 +- .../components/docs/DocsMissingPage.tsx | 110 +- frontend/src/landing/components/docs/Logo.tsx | 144 +-- .../components/docs/PlatformSupport.tsx | 153 ++- .../landing/components/docs/PluginCard.tsx | 150 +-- .../components/docs/mdx-components.tsx | 38 +- .../src/landing/content/docs/architecture.mdx | 187 ++-- frontend/src/landing/content/docs/cli.mdx | 289 +++--- .../content/docs/configuration/index.mdx | 70 +- .../content/docs/configuration/meta.json | 6 +- .../content/docs/configuration/projects.mdx | 120 ++- .../content/docs/configuration/reactions.mdx | 93 +- .../docs/configuration/remote-access.mdx | 20 +- .../src/landing/content/docs/dashboard.mdx | 76 +- .../src/landing/content/docs/examples.mdx | 18 +- frontend/src/landing/content/docs/faq.mdx | 76 +- .../content/docs/guides/ci-recovery.mdx | 12 +- .../src/landing/content/docs/guides/index.mdx | 36 +- .../src/landing/content/docs/guides/meta.json | 13 +- .../content/docs/guides/multi-project.mdx | 11 +- .../content/docs/guides/parallel-issues.mdx | 16 +- .../content/docs/guides/per-role-agents.mdx | 21 +- .../landing/content/docs/guides/reactions.mdx | 24 +- .../content/docs/guides/review-loop.mdx | 55 +- frontend/src/landing/content/docs/index.mdx | 73 +- .../src/landing/content/docs/installation.mdx | 145 ++- frontend/src/landing/content/docs/meta.json | 44 +- .../src/landing/content/docs/migration.mdx | 2 +- .../src/landing/content/docs/platforms.mdx | 86 +- .../content/docs/plugins/agents/aider.mdx | 31 +- .../docs/plugins/agents/claude-code.mdx | 16 +- .../content/docs/plugins/agents/codex.mdx | 20 +- .../content/docs/plugins/agents/cursor.mdx | 32 +- .../content/docs/plugins/agents/index.mdx | 42 +- .../content/docs/plugins/agents/meta.json | 10 +- .../content/docs/plugins/agents/opencode.mdx | 18 +- .../content/docs/plugins/authoring.mdx | 359 +++---- .../landing/content/docs/plugins/index.mdx | 176 +++- .../landing/content/docs/plugins/meta.json | 15 +- .../docs/plugins/notifiers/composio.mdx | 8 +- .../docs/plugins/notifiers/desktop.mdx | 23 +- .../docs/plugins/notifiers/discord.mdx | 8 +- .../content/docs/plugins/notifiers/index.mdx | 64 +- .../content/docs/plugins/notifiers/meta.json | 4 +- .../docs/plugins/notifiers/openclaw.mdx | 42 +- .../content/docs/plugins/notifiers/slack.mdx | 8 +- .../docs/plugins/notifiers/webhook.mdx | 42 +- .../content/docs/plugins/runtimes/index.mdx | 24 +- .../content/docs/plugins/runtimes/meta.json | 4 +- .../content/docs/plugins/runtimes/process.mdx | 6 +- .../content/docs/plugins/runtimes/tmux.mdx | 17 +- .../content/docs/plugins/scm/github.mdx | 26 +- .../content/docs/plugins/scm/gitlab.mdx | 56 +- .../content/docs/plugins/scm/index.mdx | 14 +- .../content/docs/plugins/scm/meta.json | 4 +- .../content/docs/plugins/terminals/index.mdx | 14 +- .../content/docs/plugins/terminals/iterm2.mdx | 6 +- .../content/docs/plugins/terminals/meta.json | 4 +- .../content/docs/plugins/terminals/web.mdx | 12 +- .../content/docs/plugins/trackers/github.mdx | 20 +- .../content/docs/plugins/trackers/gitlab.mdx | 17 +- .../content/docs/plugins/trackers/index.mdx | 21 +- .../content/docs/plugins/trackers/linear.mdx | 18 +- .../content/docs/plugins/trackers/meta.json | 4 +- .../content/docs/plugins/workspaces/clone.mdx | 12 +- .../content/docs/plugins/workspaces/index.mdx | 22 +- .../content/docs/plugins/workspaces/meta.json | 4 +- .../docs/plugins/workspaces/worktree.mdx | 20 +- .../src/landing/content/docs/quickstart.mdx | 57 +- .../landing/content/docs/troubleshooting.mdx | 49 +- frontend/src/landing/lib/github-repo.ts | 85 +- frontend/src/landing/lib/source.ts | 4 +- frontend/src/landing/package.json | 52 +- frontend/src/landing/postcss.config.mjs | 6 +- frontend/src/landing/source.config.ts | 26 +- frontend/src/landing/styles/globals.css | 834 +++++++++------ frontend/src/landing/tsconfig.json | 46 +- frontend/src/main.ts | 36 +- frontend/tsconfig.json | 38 +- package.json | 26 +- test/cli/README.md | 12 +- 113 files changed, 5173 insertions(+), 4674 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cd1c57df..a8fcf7e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,21 +92,25 @@ For code entry points: The daemon API is code-first. The OpenAPI spec and frontend TypeScript types are generated artifacts — edit the source, then regenerate. **Source files to edit:** + - `backend/internal/httpd/controllers/dto.go` — request/response shapes. - `backend/internal/httpd/apispec/specgen/build.go` — operation registry; add a `schemaNames` entry for any new named type. **Regenerate after editing:** + ```bash npm run api # runs api:spec then api:ts in sequence ``` This is equivalent to running: + ```bash npm run api:spec # cd backend && go generate ./internal/httpd/apispec/... npm run api:ts # npx openapi-typescript@7.4.4 backend/internal/httpd/apispec/openapi.yaml -o frontend/src/api/schema.ts ``` **Verify:** + ```bash cd backend && go test ./internal/httpd/... # spec drift + route/spec parity tests (does not cover schema.ts — that is checked by the api-drift CI job) ``` diff --git a/README.md b/README.md index 87003504..6af7923f 100644 --- a/README.md +++ b/README.md @@ -62,30 +62,30 @@ The CLI is intentionally thin: every product command resolves to a daemon HTTP route. Run `ao --help` for the authoritative flag shape; the table below groups what's on `main` today. -| Lane | Command | Purpose | -|---|---|---| -| Daemon | `ao start` | Start the daemon in the background and wait for `/readyz`. | -| Daemon | `ao stop` | Graceful shutdown via loopback `POST /shutdown`. | -| Daemon | `ao status` | Report PID/port/health/readiness from `running.json`. | -| Daemon | `ao daemon` | Hidden internal entrypoint used by `ao start`. | -| Project | `ao project add` | Register a local git repo as a project. | -| Project | `ao project ls` | List registered projects. | -| Project | `ao project get ` | Fetch one project. | -| Project | `ao project rm ` | Remove a project. | -| Session | `ao spawn` | Spawn a worker session in a registered project. | -| Session | `ao session ls` | List sessions (filter by project, include terminated). | -| Session | `ao session get ` | Fetch one session. | -| Session | `ao session kill ` | Terminate a session. | -| Session | `ao session rename ` | Rename a session. | -| Session | `ao session restore ` | Relaunch a terminated session. | -| Session | `ao session cleanup` | Reclaim eligible workspaces for terminated sessions. | -| Session | `ao session claim-pr ` | Attach an existing PR to a session. | -| Orchestrator | `ao orchestrator ls` | List orchestrator sessions. | -| Messaging | `ao send` | Send a message to a running agent session. | -| Utility | `ao doctor` | Local health checks (config, data dir, DB, `git`, `zellij`). | -| Utility | `ao completion ` | Generate bash/zsh/fish/powershell completions. | -| Utility | `ao version` | Print build metadata. | -| Internal | `ao hooks ` | Hidden adapter hook callback. | +| Lane | Command | Purpose | +| ------------ | ------------------------------------ | ------------------------------------------------------------ | +| Daemon | `ao start` | Start the daemon in the background and wait for `/readyz`. | +| Daemon | `ao stop` | Graceful shutdown via loopback `POST /shutdown`. | +| Daemon | `ao status` | Report PID/port/health/readiness from `running.json`. | +| Daemon | `ao daemon` | Hidden internal entrypoint used by `ao start`. | +| Project | `ao project add` | Register a local git repo as a project. | +| Project | `ao project ls` | List registered projects. | +| Project | `ao project get ` | Fetch one project. | +| Project | `ao project rm ` | Remove a project. | +| Session | `ao spawn` | Spawn a worker session in a registered project. | +| Session | `ao session ls` | List sessions (filter by project, include terminated). | +| Session | `ao session get ` | Fetch one session. | +| Session | `ao session kill ` | Terminate a session. | +| Session | `ao session rename ` | Rename a session. | +| Session | `ao session restore ` | Relaunch a terminated session. | +| Session | `ao session cleanup` | Reclaim eligible workspaces for terminated sessions. | +| Session | `ao session claim-pr ` | Attach an existing PR to a session. | +| Orchestrator | `ao orchestrator ls` | List orchestrator sessions. | +| Messaging | `ao send` | Send a message to a running agent session. | +| Utility | `ao doctor` | Local health checks (config, data dir, DB, `git`, `zellij`). | +| Utility | `ao completion ` | Generate bash/zsh/fish/powershell completions. | +| Utility | `ao version` | Print build metadata. | +| Internal | `ao hooks ` | Hidden adapter hook callback. | See [`docs/cli/`](docs/cli/) for the daemon-control intent and command shape. @@ -95,16 +95,16 @@ All configuration is env-driven; the daemon takes no config file. The bind host is hard-coded to `127.0.0.1` — the daemon has no auth, CORS, or TLS, and exposing it beyond loopback would be a security regression. -| Var | Default | Purpose | -|---|---|---| -| `AO_PORT` | `3001` | Bind port; daemon fails fast if taken. | -| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout (Go duration). | -| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful-shutdown hard cap. | -| `AO_RUN_FILE` | `/agent-orchestrator/running.json` | PID + port handshake path. | -| `AO_DATA_DIR` | `/agent-orchestrator/data` | SQLite DB, WAL files, managed state. | -| `AO_AGENT` | `claude-code` | Default agent adapter id used by `ao spawn`. | -| `AO_SESSION_ID` | _(unset)_ | Set inside spawned sessions; read by `ao send` and `ao hooks`. | -| `GITHUB_TOKEN` | _(unset)_ | Used by the GitHub SCM and tracker adapters. Falls back to `gh auth token`. | +| Var | Default | Purpose | +| --------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- | +| `AO_PORT` | `3001` | Bind port; daemon fails fast if taken. | +| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout (Go duration). | +| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful-shutdown hard cap. | +| `AO_RUN_FILE` | `/agent-orchestrator/running.json` | PID + port handshake path. | +| `AO_DATA_DIR` | `/agent-orchestrator/data` | SQLite DB, WAL files, managed state. | +| `AO_AGENT` | `claude-code` | Default agent adapter id used by `ao spawn`. | +| `AO_SESSION_ID` | _(unset)_ | Set inside spawned sessions; read by `ao send` and `ao hooks`. | +| `GITHUB_TOKEN` | _(unset)_ | Used by the GitHub SCM and tracker adapters. Falls back to `gh auth token`. | Health check: diff --git a/docs/README.md b/docs/README.md index 9a4e5d43..10608768 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,13 +10,13 @@ Start with [architecture.md](architecture.md) for the current backend model and ## Reference docs -| Doc | What it covers | -|-----|----------------| -| [architecture.md](architecture.md) | Current backend model, package layout, status derivation, persistence/CDC, and load-bearing rules. | +| Doc | What it covers | +| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | +| [architecture.md](architecture.md) | Current backend model, package layout, status derivation, persistence/CDC, and load-bearing rules. | | [backend-code-structure.md](backend-code-structure.md) | Package ownership rules for the Go backend: domain, services, ports, adapters, storage, HTTP, CLI, and daemon wiring. | -| [cli/README.md](cli/README.md) | CLI commands and daemon control surface. | -| [status.md](status.md) | Current implementation shape, build/test command, and next integration work. | -| [stack.md](stack.md) | Accepted library/runtime choices, pending stack decisions, and dependencies explicitly avoided for V1. | +| [cli/README.md](cli/README.md) | CLI commands and daemon control surface. | +| [status.md](status.md) | Current implementation shape, build/test command, and next integration work. | +| [stack.md](stack.md) | Accepted library/runtime choices, pending stack decisions, and dependencies explicitly avoided for V1. | ## Mental model diff --git a/docs/agent/README.md b/docs/agent/README.md index e17aa792..55d1e506 100644 --- a/docs/agent/README.md +++ b/docs/agent/README.md @@ -90,7 +90,6 @@ The workspace adapter prefers: Hook metadata changes publish `session.updated`. The frontend listens to `session.created`, `session.terminated`, and `session.updated` and invalidates the workspace query. - ## Acceptance Criteria Agent adapter behavior: diff --git a/docs/cli/README.md b/docs/cli/README.md index f4af7107..923854c3 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -7,17 +7,17 @@ call runtime, workspace, tracker, or agent adapters in-process. ## Current commands -| Command | Purpose | -|---|---| -| `ao start` | Start the daemon in the background and wait for `/readyz`. | -| `ao status` | Report daemon state from `running.json`, process liveness, `/healthz`, and `/readyz`. | -| `ao status --json` | Emit the same daemon state as machine-readable JSON. | -| `ao stop` | Gracefully stop the daemon via loopback `POST /shutdown` after verifying daemon identity. | -| `ao doctor` | Check config, data directory, DB-file presence, daemon state, `git`, and optional `zellij`. | -| `ao doctor --json` | Emit doctor checks as JSON. | -| `ao completion ` | Generate completions for `bash`, `zsh`, `fish`, or `powershell`. | -| `ao version` / `ao --version` | Print build metadata. | -| `ao daemon` | Hidden internal daemon entrypoint used by `ao start`. | +| Command | Purpose | +| ----------------------------- | ------------------------------------------------------------------------------------------- | +| `ao start` | Start the daemon in the background and wait for `/readyz`. | +| `ao status` | Report daemon state from `running.json`, process liveness, `/healthz`, and `/readyz`. | +| `ao status --json` | Emit the same daemon state as machine-readable JSON. | +| `ao stop` | Gracefully stop the daemon via loopback `POST /shutdown` after verifying daemon identity. | +| `ao doctor` | Check config, data directory, DB-file presence, daemon state, `git`, and optional `zellij`. | +| `ao doctor --json` | Emit doctor checks as JSON. | +| `ao completion ` | Generate completions for `bash`, `zsh`, `fish`, or `powershell`. | +| `ao version` / `ao --version` | Print build metadata. | +| `ao daemon` | Hidden internal daemon entrypoint used by `ao start`. | `go run .` in `backend/` remains a compatibility wrapper around the daemon. @@ -25,13 +25,13 @@ call runtime, workspace, tracker, or agent adapters in-process. The CLI and daemon share the same environment-driven config: -| Var | Default | Purpose | -|---|---|---| -| `AO_PORT` | `3001` | Loopback daemon port. | -| `AO_RUN_FILE` | `/agent-orchestrator/running.json` | PID/port handshake. | -| `AO_DATA_DIR` | `/agent-orchestrator/data` | SQLite data directory. | -| `AO_REQUEST_TIMEOUT` | `60s` | REST request timeout. | -| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap. | +| Var | Default | Purpose | +| --------------------- | ------------------------------------------------- | ---------------------- | +| `AO_PORT` | `3001` | Loopback daemon port. | +| `AO_RUN_FILE` | `/agent-orchestrator/running.json` | PID/port handshake. | +| `AO_DATA_DIR` | `/agent-orchestrator/data` | SQLite data directory. | +| `AO_REQUEST_TIMEOUT` | `60s` | REST request timeout. | +| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap. | The daemon always binds `127.0.0.1`. diff --git a/docs/design/per-project-config.md b/docs/design/per-project-config.md index f8e29080..05af2850 100644 --- a/docs/design/per-project-config.md +++ b/docs/design/per-project-config.md @@ -40,24 +40,24 @@ rather than an escape-hatch map. ## Field catalog (legacy `projects.`) and target home -| YAML field | Type | Storage today | Target | -|---|---|---|---| -| `name` | string | `projects.display_name` | done | -| `repo` | string | `projects.repo_origin_url` | done | -| `path` | string | `projects.path` | done | -| `defaultBranch` | string | hardcoded `"main"` | `projects.default_branch` | -| `sessionPrefix` | string | derived | `projects.session_prefix` | -| `agentConfig` | `{model, permissions}` | **`projects.agent_config` (typed)** | **done (this PR)** | -| `orchestrator`/`worker` overrides | `{agent, agentConfig}` | — | typed role-override columns/blob | -| `env` | `map[string]string` | — | `project_env` table (key/value rows) | -| `symlinks` | `[]string` | — | `projects.symlinks` (JSON) | -| `postCreate` | `[]string` | — | `projects.post_create` (JSON) | -| `agentRules` / `agentRulesFile` | string | partial (`SpawnConfig.AgentRules`) | `projects.agent_rules*` | -| `orchestratorRules` | string | — | `projects.orchestrator_rules` | -| `tracker` | `{plugin, …}` | DTO stub only | `projects.tracker` (typed blob) + adapter validation | -| `scm` | `{plugin, webhook{…}}` | DTO stub only | `projects.scm` (typed blob) + adapter validation | -| `opencodeIssueSessionStrategy` | enum | — | `projects.opencode_session_strategy` | -| `reactions` | per-project overrides | — | `project_reactions` (own slice) | +| YAML field | Type | Storage today | Target | +| --------------------------------- | ---------------------- | ----------------------------------- | ---------------------------------------------------- | +| `name` | string | `projects.display_name` | done | +| `repo` | string | `projects.repo_origin_url` | done | +| `path` | string | `projects.path` | done | +| `defaultBranch` | string | hardcoded `"main"` | `projects.default_branch` | +| `sessionPrefix` | string | derived | `projects.session_prefix` | +| `agentConfig` | `{model, permissions}` | **`projects.agent_config` (typed)** | **done (this PR)** | +| `orchestrator`/`worker` overrides | `{agent, agentConfig}` | — | typed role-override columns/blob | +| `env` | `map[string]string` | — | `project_env` table (key/value rows) | +| `symlinks` | `[]string` | — | `projects.symlinks` (JSON) | +| `postCreate` | `[]string` | — | `projects.post_create` (JSON) | +| `agentRules` / `agentRulesFile` | string | partial (`SpawnConfig.AgentRules`) | `projects.agent_rules*` | +| `orchestratorRules` | string | — | `projects.orchestrator_rules` | +| `tracker` | `{plugin, …}` | DTO stub only | `projects.tracker` (typed blob) + adapter validation | +| `scm` | `{plugin, webhook{…}}` | DTO stub only | `projects.scm` (typed blob) + adapter validation | +| `opencodeIssueSessionStrategy` | enum | — | `projects.opencode_session_strategy` | +| `reactions` | per-project overrides | — | `project_reactions` (own slice) | ## Typed model @@ -112,7 +112,7 @@ agent adapter. ## Sequencing (one slice per PR) -1. **agentConfig (typed)** — *this PR*. Establishes the typed+validated+surfaced +1. **agentConfig (typed)** — _this PR_. Establishes the typed+validated+surfaced pattern end to end. 2. **Project identity scalars** — `default_branch`, `session_prefix` (stop hardcoding/deriving them). diff --git a/docs/stack.md b/docs/stack.md index 334c9a59..afb59855 100644 --- a/docs/stack.md +++ b/docs/stack.md @@ -18,27 +18,27 @@ invariants. ## Accepted stack -| Area | Decision | Status | Rationale | -|------|----------|--------|-----------| -| Backend language | Go 1.25.7 | Implemented | Matches `backend/go.mod`; small daemon, strong stdlib, easy local distribution. | -| Backend core | Go stdlib | Implemented | Domain, lifecycle, session, and adapter contracts should stay dependency-light. | -| Frontend shell | Electron + TypeScript | Implemented | Local desktop control plane paired with the daemon. | -| Runtime adapter | `zellij` CLI via `os/exec` | Implemented | Terminal multiplexing fits long-running sessions, attach/debug workflows, and adapter isolation. | -| Terminal PTY | `github.com/creack/pty` | Implemented | PTY-backed terminal sessions with resize/input/output control. | -| Git/worktrees | `git` CLI via `os/exec` | Implemented | Uses real repo behavior, credentials, hooks, LFS, submodules, and user config. | -| HTTP API | `net/http` + `github.com/go-chi/chi/v5` | Implemented | Lightweight, idiomatic router without committing AO to a large web framework. | -| WebSocket | `github.com/coder/websocket` | Implemented | Small WebSocket library for terminal streaming. | -| Storage | SQLite in WAL mode via `database/sql` | Implemented | Local daemon, single writer, many dashboard/API reads, no external DB setup. | -| SQLite driver | `modernc.org/sqlite` | Implemented | Current pure-Go driver in `backend/internal/storage/sqlite`; keep it swappable behind `database/sql`. | -| SQL generation | `github.com/sqlc-dev/sqlc` | Implemented | Hand-written SQL with generated typed methods from `backend/sqlc.yaml`. | -| Migrations | `github.com/pressly/goose/v3` | Implemented | Simple SQL migrations for the embedded/local database. | -| CLI | `github.com/spf13/cobra` | Implemented | Standard command structure for daemon startup, diagnostics, and admin commands. | -| Config | stdlib environment loading + SQLite-backed state/config | Implemented / evolving | `internal/config` handles daemon env/defaults; durable product config belongs in SQLite, so no config framework is selected for V1. | -| Logging | `log/slog` | Implemented | Stdlib structured logging before adding another logging dependency. | -| OpenAPI generation | `github.com/swaggest/openapi-go`, `github.com/swaggest/jsonschema-go`, `gopkg.in/yaml.v3` | Implemented | Generated OpenAPI keeps route contracts close to Go DTOs. | -| Testing | stdlib `testing` | Implemented | Keep pure domain logic and adapter contracts easy to test. | -| Test assertions | `github.com/stretchr/testify/require` | Planned if needed | Concise assertions for higher-level adapter and integration tests; do not add unless tests benefit. | -| Packaging | `github.com/goreleaser/goreleaser` | Planned | Cross-platform release automation, checksums, and future Homebrew support. | +| Area | Decision | Status | Rationale | +| ------------------ | ----------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Backend language | Go 1.25.7 | Implemented | Matches `backend/go.mod`; small daemon, strong stdlib, easy local distribution. | +| Backend core | Go stdlib | Implemented | Domain, lifecycle, session, and adapter contracts should stay dependency-light. | +| Frontend shell | Electron + TypeScript | Implemented | Local desktop control plane paired with the daemon. | +| Runtime adapter | `zellij` CLI via `os/exec` | Implemented | Terminal multiplexing fits long-running sessions, attach/debug workflows, and adapter isolation. | +| Terminal PTY | `github.com/creack/pty` | Implemented | PTY-backed terminal sessions with resize/input/output control. | +| Git/worktrees | `git` CLI via `os/exec` | Implemented | Uses real repo behavior, credentials, hooks, LFS, submodules, and user config. | +| HTTP API | `net/http` + `github.com/go-chi/chi/v5` | Implemented | Lightweight, idiomatic router without committing AO to a large web framework. | +| WebSocket | `github.com/coder/websocket` | Implemented | Small WebSocket library for terminal streaming. | +| Storage | SQLite in WAL mode via `database/sql` | Implemented | Local daemon, single writer, many dashboard/API reads, no external DB setup. | +| SQLite driver | `modernc.org/sqlite` | Implemented | Current pure-Go driver in `backend/internal/storage/sqlite`; keep it swappable behind `database/sql`. | +| SQL generation | `github.com/sqlc-dev/sqlc` | Implemented | Hand-written SQL with generated typed methods from `backend/sqlc.yaml`. | +| Migrations | `github.com/pressly/goose/v3` | Implemented | Simple SQL migrations for the embedded/local database. | +| CLI | `github.com/spf13/cobra` | Implemented | Standard command structure for daemon startup, diagnostics, and admin commands. | +| Config | stdlib environment loading + SQLite-backed state/config | Implemented / evolving | `internal/config` handles daemon env/defaults; durable product config belongs in SQLite, so no config framework is selected for V1. | +| Logging | `log/slog` | Implemented | Stdlib structured logging before adding another logging dependency. | +| OpenAPI generation | `github.com/swaggest/openapi-go`, `github.com/swaggest/jsonschema-go`, `gopkg.in/yaml.v3` | Implemented | Generated OpenAPI keeps route contracts close to Go DTOs. | +| Testing | stdlib `testing` | Implemented | Keep pure domain logic and adapter contracts easy to test. | +| Test assertions | `github.com/stretchr/testify/require` | Planned if needed | Concise assertions for higher-level adapter and integration tests; do not add unless tests benefit. | +| Packaging | `github.com/goreleaser/goreleaser` | Planned | Cross-platform release automation, checksums, and future Homebrew support. | ## Pending decisions @@ -70,15 +70,15 @@ config surface appears. ## Explicitly avoided for V1 -| Avoid | Reason | -|-------|--------| -| GORM | AO needs explicit transactional SQL and CDC-triggered writes. | -| Gin/Fiber | `net/http` + `chi` is enough for a local daemon API. | -| `go-git` as the primary Git engine | AO should match installed Git behavior, credentials, hooks, LFS, submodules, and user config. | -| `github.com/spf13/viper` / `github.com/knadh/koanf` by default | Env/default loading plus SQLite-backed config is enough for V1. | -| Temporal / NATS / Kafka / Redis | V1 is a local daemon with SQLite and CDC, not a distributed control plane. | -| Full plugin framework | Keep adapter interfaces narrow until product needs justify a plugin runtime. | -| Multi-sink CDC fan-out | Start with one durable local delivery path; add fan-out later if needed. | +| Avoid | Reason | +| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| GORM | AO needs explicit transactional SQL and CDC-triggered writes. | +| Gin/Fiber | `net/http` + `chi` is enough for a local daemon API. | +| `go-git` as the primary Git engine | AO should match installed Git behavior, credentials, hooks, LFS, submodules, and user config. | +| `github.com/spf13/viper` / `github.com/knadh/koanf` by default | Env/default loading plus SQLite-backed config is enough for V1. | +| Temporal / NATS / Kafka / Redis | V1 is a local daemon with SQLite and CDC, not a distributed control plane. | +| Full plugin framework | Keep adapter interfaces narrow until product needs justify a plugin runtime. | +| Multi-sink CDC fan-out | Start with one durable local delivery path; add fan-out later if needed. | ## Current stack mapping diff --git a/frontend/package.json b/frontend/package.json index b58407e8..5e606b82 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,16 +1,16 @@ { - "name": "agent-orchestrator-frontend", - "version": "0.0.0", - "private": true, - "description": "Electron + TypeScript frontend for the agent-orchestrator rewrite", - "main": "dist/main.js", - "scripts": { - "build": "tsc", - "typecheck": "tsc --noEmit", - "start": "npm run build && electron ." - }, - "devDependencies": { - "electron": "^33.0.0", - "typescript": "^5.6.0" - } + "name": "agent-orchestrator-frontend", + "version": "0.0.0", + "private": true, + "description": "Electron + TypeScript frontend for the agent-orchestrator rewrite", + "main": "dist/main.js", + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "start": "npm run build && electron ." + }, + "devDependencies": { + "electron": "^33.0.0", + "typescript": "^5.6.0" + } } diff --git a/frontend/src/landing/app/docs/404/page.tsx b/frontend/src/landing/app/docs/404/page.tsx index 48c771ae..147ad5fd 100644 --- a/frontend/src/landing/app/docs/404/page.tsx +++ b/frontend/src/landing/app/docs/404/page.tsx @@ -2,14 +2,14 @@ import type { Metadata } from "next"; import { DocsMissingPage } from "@/components/docs/DocsMissingPage"; export const metadata: Metadata = { - title: "Docs page not found", - description: "This docs page moved or does not exist.", - robots: { - index: false, - follow: true, - }, + title: "Docs page not found", + description: "This docs page moved or does not exist.", + robots: { + index: false, + follow: true, + }, }; export default function Docs404Page() { - return ; + return ; } diff --git a/frontend/src/landing/app/docs/[[...slug]]/page.tsx b/frontend/src/landing/app/docs/[[...slug]]/page.tsx index 2294e984..b00e971c 100644 --- a/frontend/src/landing/app/docs/[[...slug]]/page.tsx +++ b/frontend/src/landing/app/docs/[[...slug]]/page.tsx @@ -1,85 +1,78 @@ import { notFound } from "next/navigation"; -import { - DocsPage, - DocsBody, - DocsTitle, - DocsDescription, -} from "fumadocs-ui/page"; +import { DocsPage, DocsBody, DocsTitle, DocsDescription } from "fumadocs-ui/page"; import type { Metadata } from "next"; import { source } from "@/lib/source"; import { getMDXComponents } from "@/components/docs/mdx-components"; interface PageProps { - params: Promise<{ slug?: string[] }>; + params: Promise<{ slug?: string[] }>; } export default async function DocsSlugPage({ params }: PageProps) { - const { slug } = await params; - const page = source.getPage(slug); - if (!page) notFound(); + const { slug } = await params; + const page = source.getPage(slug); + if (!page) notFound(); - const MDX = page.data.body; + const MDX = page.data.body; - return ( - - {page.data.title} - {page.data.description} - - - - - ); + return ( + + {page.data.title} + {page.data.description} + + + + + ); } export async function generateStaticParams() { - return source.generateParams(); + return source.generateParams(); } -export async function generateMetadata({ - params, -}: PageProps): Promise { - const { slug } = await params; - const page = source.getPage(slug); - if (!page) { - return { - title: "Docs page not found", - description: "This docs page moved or does not exist.", - robots: { - index: false, - follow: true, - }, - }; - } +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + const page = source.getPage(slug); + if (!page) { + return { + title: "Docs page not found", + description: "This docs page moved or does not exist.", + robots: { + index: false, + follow: true, + }, + }; + } - return { - title: page.data.title, - description: page.data.description, - alternates: { - canonical: `https://aoagents.dev${page.url}`, - }, - openGraph: { - title: page.data.title, - description: page.data.description, - }, - }; + return { + title: page.data.title, + description: page.data.description, + alternates: { + canonical: `https://aoagents.dev${page.url}`, + }, + openGraph: { + title: page.data.title, + description: page.data.description, + }, + }; } diff --git a/frontend/src/landing/app/docs/docs.css b/frontend/src/landing/app/docs/docs.css index 9fe5f573..1b0ff94a 100644 --- a/frontend/src/landing/app/docs/docs.css +++ b/frontend/src/landing/app/docs/docs.css @@ -12,63 +12,63 @@ ═══════════════════════════════════════════════════════════════════════════ */ :root { - /* Docs primary accent = AMBER (matches dashboard Orchestrator button + active items) */ - --docs-accent: var(--color-accent-amber); - --docs-accent-dim: var(--color-accent-amber-dim); - --docs-accent-border: var(--color-accent-amber-border); - - --color-fd-background: var(--color-bg-base); - --color-fd-foreground: var(--color-text-primary); - --color-fd-muted: var(--color-bg-elevated); - --color-fd-muted-foreground: var(--color-text-secondary); - --color-fd-popover: var(--color-bg-elevated); - --color-fd-popover-foreground: var(--color-text-primary); - --color-fd-card: var(--color-bg-surface); - --color-fd-card-foreground: var(--color-text-primary); - --color-fd-border: var(--color-border-default); - --color-fd-primary: var(--docs-accent); - --color-fd-primary-foreground: #ffffff; - --color-fd-secondary: var(--color-bg-elevated); - --color-fd-secondary-foreground: var(--color-text-primary); - --color-fd-accent: var(--docs-accent-dim); - --color-fd-accent-foreground: var(--docs-accent); - --color-fd-ring: var(--docs-accent); + /* Docs primary accent = AMBER (matches dashboard Orchestrator button + active items) */ + --docs-accent: var(--color-accent-amber); + --docs-accent-dim: var(--color-accent-amber-dim); + --docs-accent-border: var(--color-accent-amber-border); + + --color-fd-background: var(--color-bg-base); + --color-fd-foreground: var(--color-text-primary); + --color-fd-muted: var(--color-bg-elevated); + --color-fd-muted-foreground: var(--color-text-secondary); + --color-fd-popover: var(--color-bg-elevated); + --color-fd-popover-foreground: var(--color-text-primary); + --color-fd-card: var(--color-bg-surface); + --color-fd-card-foreground: var(--color-text-primary); + --color-fd-border: var(--color-border-default); + --color-fd-primary: var(--docs-accent); + --color-fd-primary-foreground: #ffffff; + --color-fd-secondary: var(--color-bg-elevated); + --color-fd-secondary-foreground: var(--color-text-primary); + --color-fd-accent: var(--docs-accent-dim); + --color-fd-accent-foreground: var(--docs-accent); + --color-fd-ring: var(--docs-accent); } .dark { - --docs-accent: var(--color-accent-amber); - --docs-accent-dim: var(--color-accent-amber-dim); - --docs-accent-border: var(--color-accent-amber-border); - - --color-fd-background: var(--color-bg-base); - --color-fd-foreground: var(--color-text-primary); - --color-fd-muted: var(--color-bg-surface); - --color-fd-muted-foreground: var(--color-text-secondary); - --color-fd-popover: var(--color-bg-elevated); - --color-fd-popover-foreground: var(--color-text-primary); - --color-fd-card: var(--color-bg-surface); - --color-fd-card-foreground: var(--color-text-primary); - --color-fd-border: var(--color-border-default); - --color-fd-primary: var(--docs-accent); - --color-fd-primary-foreground: var(--color-bg-base); - --color-fd-secondary: var(--color-bg-elevated); - --color-fd-secondary-foreground: var(--color-text-primary); - --color-fd-accent: var(--docs-accent-dim); - --color-fd-accent-foreground: var(--docs-accent); - --color-fd-ring: var(--docs-accent); + --docs-accent: var(--color-accent-amber); + --docs-accent-dim: var(--color-accent-amber-dim); + --docs-accent-border: var(--color-accent-amber-border); + + --color-fd-background: var(--color-bg-base); + --color-fd-foreground: var(--color-text-primary); + --color-fd-muted: var(--color-bg-surface); + --color-fd-muted-foreground: var(--color-text-secondary); + --color-fd-popover: var(--color-bg-elevated); + --color-fd-popover-foreground: var(--color-text-primary); + --color-fd-card: var(--color-bg-surface); + --color-fd-card-foreground: var(--color-text-primary); + --color-fd-border: var(--color-border-default); + --color-fd-primary: var(--docs-accent); + --color-fd-primary-foreground: var(--color-bg-base); + --color-fd-secondary: var(--color-bg-elevated); + --color-fd-secondary-foreground: var(--color-text-primary); + --color-fd-accent: var(--docs-accent-dim); + --color-fd-accent-foreground: var(--docs-accent); + --color-fd-ring: var(--docs-accent); } /* Sidebar-specific dark overrides */ .dark #nd-sidebar { - --color-fd-muted: var(--color-bg-surface); - --color-fd-secondary: var(--color-bg-elevated); - --color-fd-muted-foreground: var(--color-text-secondary); + --color-fd-muted: var(--color-bg-surface); + --color-fd-secondary: var(--color-bg-elevated); + --color-fd-muted-foreground: var(--color-text-secondary); } /* Sidebar background — match dashboard's very dark sidebar */ #nd-sidebar { - background-color: var(--color-bg-sidebar) !important; - border-right-color: var(--color-border-subtle) !important; + background-color: var(--color-bg-sidebar) !important; + border-right-color: var(--color-border-subtle) !important; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -76,7 +76,7 @@ ═══════════════════════════════════════════════════════════════════════════ */ #nd-docs-layout { - background: var(--color-bg-base) !important; + background: var(--color-bg-base) !important; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -84,17 +84,17 @@ ═══════════════════════════════════════════════════════════════════════════ */ :root { - --color-fd-info: var(--docs-accent); - --color-fd-warning: var(--color-accent-amber); - --color-fd-success: #16a34a; - --color-fd-error: #dc2626; + --color-fd-info: var(--docs-accent); + --color-fd-warning: var(--color-accent-amber); + --color-fd-success: #16a34a; + --color-fd-error: #dc2626; } .dark { - --color-fd-info: var(--docs-accent); - --color-fd-warning: var(--color-accent-amber); - --color-fd-success: #22c55e; - --color-fd-error: #ef4444; + --color-fd-info: var(--docs-accent); + --color-fd-warning: var(--color-accent-amber); + --color-fd-success: #22c55e; + --color-fd-error: #ef4444; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -108,75 +108,75 @@ #nd-sidebar button, #nd-sidebar-mobile a:not([data-active="true"]), #nd-sidebar-mobile button { - color: var(--color-text-secondary) !important; + color: var(--color-text-secondary) !important; } #nd-sidebar a:not([data-active="true"]):hover, #nd-sidebar button:hover, #nd-sidebar-mobile a:not([data-active="true"]):hover, #nd-sidebar-mobile button:hover { - color: var(--color-text-primary) !important; + color: var(--color-text-primary) !important; } /* Sidebar active link: amber accent (desktop + mobile) */ #nd-sidebar a[data-active="true"], #nd-sidebar-mobile a[data-active="true"] { - color: var(--docs-accent) !important; + color: var(--docs-accent) !important; } /* TOC links: warm gray, not blue */ #nd-toc a, #nd-tocnav a { - color: var(--color-text-secondary) !important; + color: var(--color-text-secondary) !important; } #nd-toc a:hover, #nd-tocnav a:hover { - color: var(--color-text-primary) !important; + color: var(--color-text-primary) !important; } #nd-toc a[data-active="true"], #nd-tocnav a[data-active="true"] { - color: var(--docs-accent) !important; + color: var(--docs-accent) !important; } /* Breadcrumb: warm gray */ #nd-page nav a, #nd-page [aria-label="breadcrumb"] a { - color: var(--color-text-secondary) !important; - text-decoration: none !important; + color: var(--color-text-secondary) !important; + text-decoration: none !important; } #nd-page nav a:hover, #nd-page [aria-label="breadcrumb"] a:hover { - color: var(--color-text-primary) !important; + color: var(--color-text-primary) !important; } /* Page title breadcrumb link (the blue "Installation" at top) */ #nd-page a[href^="/docs"] { - color: var(--color-text-secondary) !important; + color: var(--color-text-secondary) !important; } #nd-page a[href^="/docs"]:hover { - color: var(--color-text-primary) !important; + color: var(--color-text-primary) !important; } /* Step number circles: amber */ #nd-docs-layout .fd-step::before { - background: var(--docs-accent) !important; + background: var(--docs-accent) !important; } /* Footer prev/next: warm gray, not blue */ #nd-page footer a, #nd-page [class*="footer"] a { - color: var(--color-text-secondary) !important; - border-color: var(--color-border-default) !important; + color: var(--color-text-secondary) !important; + border-color: var(--color-border-default) !important; } #nd-page footer a:hover, #nd-page [class*="footer"] a:hover { - color: var(--color-text-primary) !important; - border-color: var(--docs-accent) !important; + color: var(--color-text-primary) !important; + border-color: var(--docs-accent) !important; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -184,37 +184,33 @@ ═══════════════════════════════════════════════════════════════════════════ */ #nd-docs-layout { - font-family: - var(--font-geist-sans), - ui-sans-serif, - system-ui, - -apple-system, - sans-serif; - font-size: 13px; - letter-spacing: -0.011em; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + font-family: + var(--font-geist-sans), + ui-sans-serif, + system-ui, + -apple-system, + sans-serif; + font-size: 13px; + letter-spacing: -0.011em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } #nd-docs-layout code, #nd-docs-layout kbd, #nd-docs-layout pre { - font-family: - var(--font-jetbrains-mono), - ui-monospace, - "SFMono-Regular", - monospace; + font-family: var(--font-jetbrains-mono), ui-monospace, "SFMono-Regular", monospace; } /* Sidebar — smaller text like dashboard */ #nd-sidebar { - font-size: 12.5px; + font-size: 12.5px; } /* TOC — compact */ #nd-toc, #nd-tocnav { - font-size: 11.5px; + font-size: 11.5px; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -222,28 +218,28 @@ ═══════════════════════════════════════════════════════════════════════════ */ #nd-docs-layout .prose h1 { - color: var(--color-text-primary); - font-weight: 700; - font-size: 1.65em; - letter-spacing: -0.02em; + color: var(--color-text-primary); + font-weight: 700; + font-size: 1.65em; + letter-spacing: -0.02em; } #nd-docs-layout .prose h2 { - color: var(--color-text-primary); - font-weight: 680; - font-size: 1.3em; - letter-spacing: -0.015em; + color: var(--color-text-primary); + font-weight: 680; + font-size: 1.3em; + letter-spacing: -0.015em; } #nd-docs-layout .prose h3 { - color: var(--color-text-primary); - font-weight: 640; - font-size: 1.1em; + color: var(--color-text-primary); + font-weight: 640; + font-size: 1.1em; } #nd-docs-layout .prose h4 { - color: var(--color-text-primary); - font-weight: 620; + color: var(--color-text-primary); + font-weight: 620; } /* Heading anchors — strip card border/bg */ @@ -251,51 +247,51 @@ #nd-docs-layout .prose h2 a[data-card], #nd-docs-layout .prose h3 a[data-card], #nd-docs-layout .prose h4 a[data-card] { - border: none; - background: none; - padding: 0; - color: inherit; + border: none; + background: none; + padding: 0; + color: inherit; } /* Links — amber accent like dashboard */ #nd-docs-layout .prose a { - color: var(--docs-accent); - text-decoration: none; + color: var(--docs-accent); + text-decoration: none; } #nd-docs-layout .prose a:hover { - text-decoration: underline; - text-underline-offset: 3px; + text-decoration: underline; + text-underline-offset: 3px; } /* Tables — dashboard style: uppercase headers, dense */ #nd-docs-layout .prose table { - font-size: 12.5px; + font-size: 12.5px; } #nd-docs-layout .prose th { - background-color: var(--color-bg-subtle); - color: var(--color-text-tertiary, var(--color-text-secondary)); - font-size: 10px; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; - padding: 0.5rem 0.75rem; + background-color: var(--color-bg-subtle); + color: var(--color-text-tertiary, var(--color-text-secondary)); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 0.5rem 0.75rem; } #nd-docs-layout .prose td { - padding: 0.5rem 0.75rem; + padding: 0.5rem 0.75rem; } /* Inline code */ #nd-docs-layout :not(pre) > code { - font-size: 0.85em; - padding: 0.12em 0.4em; - border-radius: 4px; - background-color: var(--color-bg-subtle); - border: 1px solid var(--color-border-subtle); - color: var(--docs-accent); - font-weight: 450; + font-size: 0.85em; + padding: 0.12em 0.4em; + border-radius: 4px; + background-color: var(--color-bg-subtle); + border: 1px solid var(--color-border-subtle); + color: var(--docs-accent); + font-weight: 450; } /* No underlines in sidebar (desktop + mobile), TOC, or nav */ @@ -306,7 +302,7 @@ #nd-toc a, #nd-tocnav a, #nd-page nav a { - text-decoration: none !important; + text-decoration: none !important; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -314,56 +310,56 @@ ═══════════════════════════════════════════════════════════════════════════ */ .shiki span { - color: var(--shiki-light) !important; - background-color: transparent !important; + color: var(--shiki-light) !important; + background-color: transparent !important; } .dark .shiki span { - color: var(--shiki-dark) !important; - background-color: transparent !important; + color: var(--shiki-dark) !important; + background-color: transparent !important; } #nd-docs-layout pre, pre.shiki, pre.shiki code { - background-color: var(--color-bg-inset, var(--color-bg-surface)) !important; + background-color: var(--color-bg-inset, var(--color-bg-surface)) !important; } #nd-docs-layout pre { - font-size: 12px; - line-height: 1.7; - overflow-x: auto; - tab-size: 2; + font-size: 12px; + line-height: 1.7; + overflow-x: auto; + tab-size: 2; } #nd-docs-layout pre:not(figure *) { - border-radius: 6px; - border: 1px solid var(--color-border-subtle); - padding: 0.875rem 1rem; + border-radius: 6px; + border: 1px solid var(--color-border-subtle); + padding: 0.875rem 1rem; } #nd-docs-layout figure pre { - border: none; - border-radius: 0; - margin: 0; + border: none; + border-radius: 0; + margin: 0; } #nd-docs-layout figure.shiki { - background-color: var(--color-bg-inset, var(--color-bg-surface)) !important; - border-color: var(--color-border-subtle) !important; - border-radius: 6px; + background-color: var(--color-bg-inset, var(--color-bg-surface)) !important; + border-color: var(--color-border-subtle) !important; + border-radius: 6px; } /* Copy button fade on hover */ #nd-docs-layout figure[data-rehype-pretty-code-figure] button, #nd-docs-layout [data-rehype-pretty-code-figure] button[aria-label] { - opacity: 0; - transition: opacity 0.15s ease; + opacity: 0; + transition: opacity 0.15s ease; } #nd-docs-layout figure[data-rehype-pretty-code-figure]:hover button, #nd-docs-layout [data-rehype-pretty-code-figure]:hover button[aria-label] { - opacity: 1; + opacity: 1; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -371,25 +367,25 @@ pre.shiki code { ═══════════════════════════════════════════════════════════════════════════ */ #nd-docs-layout [data-callout] { - border-radius: 6px; - border-left-width: 3px; - padding: 0.625rem 0.875rem; - margin: 1.25rem 0; - font-size: 12.5px; + border-radius: 6px; + border-left-width: 3px; + padding: 0.625rem 0.875rem; + margin: 1.25rem 0; + font-size: 12.5px; } /* Force info callout to amber (fumadocs uses --color-fd-info internally) */ #nd-docs-layout [data-callout][data-type="info"] { - --callout-color: var(--docs-accent) !important; + --callout-color: var(--docs-accent) !important; } /* Edit on GitHub button — force amber colors */ #nd-docs-layout a[href*="github.com"][href*="/blob/"] { - color: var(--color-text-secondary) !important; + color: var(--color-text-secondary) !important; } #nd-docs-layout a[href*="github.com"][href*="/blob/"]:hover { - color: var(--docs-accent) !important; + color: var(--docs-accent) !important; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -397,16 +393,16 @@ pre.shiki code { ═══════════════════════════════════════════════════════════════════════════ */ #nd-docs-layout pre::-webkit-scrollbar { - height: 4px; + height: 4px; } #nd-docs-layout pre::-webkit-scrollbar-track { - background: transparent; + background: transparent; } #nd-docs-layout pre::-webkit-scrollbar-thumb { - background: var(--color-scrollbar, rgba(255, 240, 220, 0.15)); - border-radius: 2px; + background: var(--color-scrollbar, rgba(255, 240, 220, 0.15)); + border-radius: 2px; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -414,9 +410,9 @@ pre.shiki code { ═══════════════════════════════════════════════════════════════════════════ */ [data-search-dialog] { - --color-fd-background: var(--color-bg-elevated); - --color-fd-border: var(--color-border-default); - --color-fd-card: var(--color-bg-surface); + --color-fd-background: var(--color-bg-elevated); + --color-fd-border: var(--color-border-default); + --color-fd-card: var(--color-bg-surface); } /* ═══════════════════════════════════════════════════════════════════════════ @@ -424,15 +420,15 @@ pre.shiki code { ═══════════════════════════════════════════════════════════════════════════ */ #nd-docs-layout .prose > * + * { - margin-top: 1em; + margin-top: 1em; } #nd-docs-layout .prose > h2 { - margin-top: 2em; + margin-top: 2em; } #nd-docs-layout .prose > h3 { - margin-top: 1.5em; + margin-top: 1.5em; } /* ═══════════════════════════════════════════════════════════════════════════ @@ -440,154 +436,157 @@ pre.shiki code { ═══════════════════════════════════════════════════════════════════════════ */ #nd-docs-layout .docs-missing-wrap { - display: flex; - min-height: calc(100vh - var(--fd-nav-height, 0px) - 8rem); - align-items: center; - padding-block: 2rem; + display: flex; + min-height: calc(100vh - var(--fd-nav-height, 0px) - 8rem); + align-items: center; + padding-block: 2rem; } #nd-docs-layout .docs-missing-card { - width: 100%; - max-width: 56rem; - margin-inline: auto; - overflow: hidden; - border: 1px solid var(--color-border-default); - border-radius: 8px; - background: var(--color-bg-surface); + width: 100%; + max-width: 56rem; + margin-inline: auto; + overflow: hidden; + border: 1px solid var(--color-border-default); + border-radius: 8px; + background: var(--color-bg-surface); } #nd-docs-layout .docs-missing-label { - padding: 0.75rem 1.5rem; - border-bottom: 1px solid var(--color-border-subtle); - background: var(--color-bg-inset); - color: var(--color-accent-amber); - font-family: var(--font-mono); - font-size: 0.6875rem; - font-weight: 600; - letter-spacing: 0.22em; - text-transform: uppercase; + padding: 0.75rem 1.5rem; + border-bottom: 1px solid var(--color-border-subtle); + background: var(--color-bg-inset); + color: var(--color-accent-amber); + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.22em; + text-transform: uppercase; } #nd-docs-layout .docs-missing-content { - display: grid; - grid-template-columns: minmax(0, 1fr) 15rem; - gap: 1.5rem; - align-items: start; - padding: 1.5rem; + display: grid; + grid-template-columns: minmax(0, 1fr) 15rem; + gap: 1.5rem; + align-items: start; + padding: 1.5rem; } #nd-docs-layout .docs-missing-copy h2 { - max-width: 42rem; - margin: 0; - color: var(--color-text-primary); - font-size: 1.875rem; - font-weight: 650; - line-height: 1.15; - letter-spacing: 0; + max-width: 42rem; + margin: 0; + color: var(--color-text-primary); + font-size: 1.875rem; + font-weight: 650; + line-height: 1.15; + letter-spacing: 0; } #nd-docs-layout .docs-missing-copy p { - max-width: 42rem; - margin: 0.75rem 0 0; - color: var(--color-text-secondary); - font-size: 0.875rem; - line-height: 1.65; + max-width: 42rem; + margin: 0.75rem 0 0; + color: var(--color-text-secondary); + font-size: 0.875rem; + line-height: 1.65; } #nd-docs-layout .docs-missing-actions { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - margin-top: 1.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.5rem; } #nd-docs-layout .docs-missing-primary, #nd-docs-layout .docs-missing-secondary { - display: inline-flex; - height: 2.5rem; - align-items: center; - justify-content: center; - border-radius: 6px; - padding: 0 1rem; - font-size: 0.875rem; - font-weight: 600; - text-decoration: none; - transition: border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease; + display: inline-flex; + height: 2.5rem; + align-items: center; + justify-content: center; + border-radius: 6px; + padding: 0 1rem; + font-size: 0.875rem; + font-weight: 600; + text-decoration: none; + transition: + border-color 0.15s ease, + color 0.15s ease, + opacity 0.15s ease; } #nd-docs-layout .docs-missing-primary { - background: var(--color-accent-amber); - color: #1a1918; + background: var(--color-accent-amber); + color: #1a1918; } #nd-docs-layout .docs-missing-primary:hover { - opacity: 0.9; + opacity: 0.9; } #nd-docs-layout .docs-missing-secondary { - border: 1px solid var(--color-border-default); - color: var(--color-text-secondary); + border: 1px solid var(--color-border-default); + color: var(--color-text-secondary); } #nd-docs-layout .docs-missing-secondary:hover { - color: var(--color-text-primary); + color: var(--color-text-primary); } #nd-docs-layout .docs-missing-terminal { - min-width: 0; - padding: 1rem; - border: 1px solid var(--color-border-subtle); - border-radius: 6px; - background: var(--color-bg-inset); - color: var(--color-text-secondary); - font-family: var(--font-mono); - font-size: 0.75rem; - line-height: 1.6; + min-width: 0; + padding: 1rem; + border: 1px solid var(--color-border-subtle); + border-radius: 6px; + background: var(--color-bg-inset); + color: var(--color-text-secondary); + font-family: var(--font-mono); + font-size: 0.75rem; + line-height: 1.6; } #nd-docs-layout .docs-missing-terminal p { - margin: 0; + margin: 0; } #nd-docs-layout .docs-missing-terminal p + p { - margin-top: 0.375rem; + margin-top: 0.375rem; } #nd-docs-layout .docs-missing-dots { - display: flex; - gap: 0.375rem; - margin-bottom: 0.75rem; + display: flex; + gap: 0.375rem; + margin-bottom: 0.75rem; } #nd-docs-layout .docs-missing-dots span { - width: 0.5rem; - height: 0.5rem; - border-radius: 999px; + width: 0.5rem; + height: 0.5rem; + border-radius: 999px; } #nd-docs-layout .docs-missing-dot-red { - background: #ef4444; + background: #ef4444; } #nd-docs-layout .docs-missing-dot-yellow { - background: #f59e0b; + background: #f59e0b; } #nd-docs-layout .docs-missing-dot-green { - background: #22c55e; + background: #22c55e; } #nd-docs-layout .docs-missing-command { - color: var(--color-text-tertiary); + color: var(--color-text-tertiary); } #nd-docs-layout .docs-missing-status { - color: var(--color-text-primary); - font-weight: 600; + color: var(--color-text-primary); + font-weight: 600; } @media (max-width: 900px) { - #nd-docs-layout .docs-missing-content { - grid-template-columns: 1fr; - } + #nd-docs-layout .docs-missing-content { + grid-template-columns: 1fr; + } } diff --git a/frontend/src/landing/app/docs/layout.tsx b/frontend/src/landing/app/docs/layout.tsx index 68e0ec3c..7813f17d 100644 --- a/frontend/src/landing/app/docs/layout.tsx +++ b/frontend/src/landing/app/docs/layout.tsx @@ -6,124 +6,106 @@ import { source } from "@/lib/source"; import "./docs.css"; function GithubIcon({ size = 16 }: { size?: number } = {}) { - return ( - - - - ); + return ( + + + + ); } function XIcon() { - return ( - - - - ); + return ( + + + + ); } function DiscordIcon() { - return ( - - - - ); + return ( + + + + ); } const links: LinkItemType[] = [ - { - type: "icon", - label: "X (Twitter)", - icon: , - text: "X", - url: "https://x.com/aoagents", - external: true, - }, - { - type: "icon", - label: "Discord", - icon: , - text: "Discord", - url: "https://discord.gg/UZv7JjxbwG", - external: true, - }, + { + type: "icon", + label: "X (Twitter)", + icon: , + text: "X", + url: "https://x.com/aoagents", + external: true, + }, + { + type: "icon", + label: "Discord", + icon: , + text: "Discord", + url: "https://discord.gg/UZv7JjxbwG", + external: true, + }, ]; async function GitHubStars() { - let stars: string | null = null; - try { - const res = await fetch( - "https://api.github.com/repos/ComposioHQ/agent-orchestrator", - { next: { revalidate: 3600 } }, - ); - if (res.ok) { - const data = await res.json(); - const count = data.stargazers_count as number; - stars = count >= 1000 ? `${(count / 1000).toFixed(1)}K` : String(count); - } - } catch { - /* GitHub API failed — hide stars */ - } + let stars: string | null = null; + try { + const res = await fetch("https://api.github.com/repos/ComposioHQ/agent-orchestrator", { + next: { revalidate: 3600 }, + }); + if (res.ok) { + const data = await res.json(); + const count = data.stargazers_count as number; + stars = count >= 1000 ? `${(count / 1000).toFixed(1)}K` : String(count); + } + } catch { + /* GitHub API failed — hide stars */ + } - return stars ? ( - - - - - {stars} - - ) : null; + return stars ? ( + + + + + {stars} + + ) : null; } export default function Layout({ children }: { children: ReactNode }) { - return ( - - - - AO - - ), - }} - sidebar={{ - defaultOpenLevel: 1, - collapsible: true, - banner: ( - - - ComposioHQ/agent-orchestrator - - - ), - }} - > - {children} - - - ); -} \ No newline at end of file + return ( + + + + AO + + ), + }} + sidebar={{ + defaultOpenLevel: 1, + collapsible: true, + banner: ( + + + ComposioHQ/agent-orchestrator + + + ), + }} + > + {children} + + + ); +} diff --git a/frontend/src/landing/app/docs/not-found.tsx b/frontend/src/landing/app/docs/not-found.tsx index 7569c68e..bebcb7ce 100644 --- a/frontend/src/landing/app/docs/not-found.tsx +++ b/frontend/src/landing/app/docs/not-found.tsx @@ -1,5 +1,5 @@ import { DocsMissingPage } from "@/components/docs/DocsMissingPage"; export default function DocsNotFound() { - return ; + return ; } diff --git a/frontend/src/landing/app/landing/layout.tsx b/frontend/src/landing/app/landing/layout.tsx index 850390d9..cc782ff1 100644 --- a/frontend/src/landing/app/landing/layout.tsx +++ b/frontend/src/landing/app/landing/layout.tsx @@ -1,32 +1,32 @@ import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Agent Orchestrator", - description: - "Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.", - openGraph: { - type: "website", - url: "https://aoagents.dev/landing", - siteName: "Agent Orchestrator", - title: "Agent Orchestrator", - description: - "Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.", - images: [{ url: "/og-image.png", width: 1024, height: 1024, alt: "Agent Orchestrator" }], - }, - twitter: { - card: "summary", - site: "@aoagents", - creator: "@aoagents", - title: "Agent Orchestrator", - description: - "Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.", - images: ["/og-image.png"], - }, - alternates: { - canonical: "https://aoagents.dev/", - }, + title: "Agent Orchestrator", + description: + "Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.", + openGraph: { + type: "website", + url: "https://aoagents.dev/landing", + siteName: "Agent Orchestrator", + title: "Agent Orchestrator", + description: + "Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.", + images: [{ url: "/og-image.png", width: 1024, height: 1024, alt: "Agent Orchestrator" }], + }, + twitter: { + card: "summary", + site: "@aoagents", + creator: "@aoagents", + title: "Agent Orchestrator", + description: + "Open-source platform for running parallel AI coding agents. Spawn Claude Code, Codex, Aider, and more in isolated worktrees — all managed from one dashboard.", + images: ["/og-image.png"], + }, + alternates: { + canonical: "https://aoagents.dev/", + }, }; export default function LandingLayout({ children }: { children: React.ReactNode }) { - return <>{children}; + return <>{children}; } diff --git a/frontend/src/landing/app/landing/page.tsx b/frontend/src/landing/app/landing/page.tsx index a9eb619d..39c6c5d1 100644 --- a/frontend/src/landing/app/landing/page.tsx +++ b/frontend/src/landing/app/landing/page.tsx @@ -16,29 +16,35 @@ import { PageConstellation } from "../../components/PageConstellation"; import { formatCompactNumber, getGitHubRepoStats } from "../../lib/github-repo"; export default async function LandingPage() { - const githubStats = await getGitHubRepoStats(); + const githubStats = await getGitHubRepoStats(); - return ( - - -
- - - - - -
-
- - - - -
- -
- MIT Licensed · Open Source -
-
-
- ); + return ( + + +
+ + + + + +
+ +
+
+ +
+ + + + +
+ +
+ +
+ MIT Licensed · Open Source +
+
+
+ ); } diff --git a/frontend/src/landing/app/layout.tsx b/frontend/src/landing/app/layout.tsx index 38b58674..67420c7a 100644 --- a/frontend/src/landing/app/layout.tsx +++ b/frontend/src/landing/app/layout.tsx @@ -2,14 +2,14 @@ import type { Metadata } from "next"; import "../styles/globals.css"; export const metadata: Metadata = { - title: "Agent Orchestrator", - description: "Open-source platform for running parallel AI coding agents.", + title: "Agent Orchestrator", + description: "Open-source platform for running parallel AI coding agents.", }; export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/frontend/src/landing/app/page.tsx b/frontend/src/landing/app/page.tsx index 1e6d70b5..9d866e37 100644 --- a/frontend/src/landing/app/page.tsx +++ b/frontend/src/landing/app/page.tsx @@ -1,9 +1,9 @@ import LandingPage from "./landing/page"; export default function HomePage() { - return ( -
- -
- ); -} \ No newline at end of file + return ( +
+ +
+ ); +} diff --git a/frontend/src/landing/components/LandingAbout.tsx b/frontend/src/landing/components/LandingAbout.tsx index b59c02f4..35ad4275 100644 --- a/frontend/src/landing/components/LandingAbout.tsx +++ b/frontend/src/landing/components/LandingAbout.tsx @@ -1,54 +1,60 @@ export function LandingAbout() { - return ( -
-
-
- The problem -
-

- You're running AI agents in 10 browser tabs.{" "} - - Checking if PRs landed. Re-running failed CI. Copy-pasting error logs. - -

+ return ( +
+
+
+ The problem +
+

+ You're running AI agents in 10 browser tabs.{" "} + + Checking if PRs landed. Re-running failed CI. Copy-pasting error logs. + +

-
-

- Agent Orchestrator replaces that with one YAML file. Point it at - your GitHub issues, pick your agents, and walk away. Each agent - spawns in its own git worktree, creates PRs, fixes CI failures, - addresses review comments, and moves toward merge. If you are new, start with the docs quickstart and configuration guides. -

+
+

+ Agent Orchestrator replaces that with one YAML file. Point it at your GitHub issues, pick your agents, and + walk away. Each agent spawns in its own git worktree, creates PRs, fixes CI failures, addresses review + comments, and moves toward merge. If you are new, start with the{" "} + + docs quickstart and configuration guides + + . +

- {/* Config preview — show how simple setup is */} -
-
-
-
-
- - agent-orchestrator.yaml - -
-
-              agent:{" "}
-              claude-code
-              {"\n"}
-              tracker:{" "}
-              github
-              {"\n"}
-              workspace:{" "}
-              worktree
-              {"\n"}
-              runtime:{" "}
-              tmux
-              {"\n"}
-              notifier:{" "}
-              slack
-            
-
-
-
-
- ); + {/* Config preview — show how simple setup is */} +
+
+
+
+
+ + agent-orchestrator.yaml + +
+
+							agent:{" "}
+							claude-code
+							{"\n"}
+							tracker:{" "}
+							github
+							{"\n"}
+							workspace:{" "}
+							worktree
+							{"\n"}
+							runtime:{" "}
+							tmux
+							{"\n"}
+							notifier:{" "}
+							slack
+						
+
+
+
+
+ ); } diff --git a/frontend/src/landing/components/LandingAgentsBar.tsx b/frontend/src/landing/components/LandingAgentsBar.tsx index 1dcd5242..5f38902d 100644 --- a/frontend/src/landing/components/LandingAgentsBar.tsx +++ b/frontend/src/landing/components/LandingAgentsBar.tsx @@ -1,51 +1,45 @@ const agents = [ - { - name: "Claude Code", - src: "/docs/logos/claude-code.svg", - alt: "Anthropic", - }, - { - name: "Codex", - src: "/docs/logos/codex.svg", - alt: "OpenAI", - }, - { - name: "Cursor", - src: "/docs/logos/cursor.svg", - alt: "Cursor", - }, - { - name: "Aider", - src: "https://aider.chat/assets/logo.svg", - alt: "Aider", - }, - { - name: "OpenCode", - src: "/docs/logos/opencode.svg", - alt: "OpenCode", - }, + { + name: "Claude Code", + src: "/docs/logos/claude-code.svg", + alt: "Anthropic", + }, + { + name: "Codex", + src: "/docs/logos/codex.svg", + alt: "OpenAI", + }, + { + name: "Cursor", + src: "/docs/logos/cursor.svg", + alt: "Cursor", + }, + { + name: "Aider", + src: "https://aider.chat/assets/logo.svg", + alt: "Aider", + }, + { + name: "OpenCode", + src: "/docs/logos/opencode.svg", + alt: "OpenCode", + }, ]; export function LandingAgentsBar() { - return ( -
-
- Works with your favorite AI agents -
-
- {agents.map((agent) => ( -
- {agent.alt} -
- {agent.name} -
-
- ))} -
-
- ); + return ( +
+
+ Works with your favorite AI agents +
+
+ {agents.map((agent) => ( +
+ {agent.alt} +
{agent.name}
+
+ ))} +
+
+ ); } diff --git a/frontend/src/landing/components/LandingCTA.tsx b/frontend/src/landing/components/LandingCTA.tsx index d1b2c3df..2a1d5e6f 100644 --- a/frontend/src/landing/components/LandingCTA.tsx +++ b/frontend/src/landing/components/LandingCTA.tsx @@ -1,33 +1,33 @@ export function LandingCTA() { - return ( -
-
-

- Stop babysitting. -

-

- Start orchestrating. -

-
- $ npm i -g @aoagents/ao -
- -
-
- ); + return ( +
+
+

+ Stop babysitting. +

+

+ Start orchestrating. +

+
+ $ npm i -g @aoagents/ao +
+ +
+
+ ); } diff --git a/frontend/src/landing/components/LandingDifferentiators.tsx b/frontend/src/landing/components/LandingDifferentiators.tsx index d104b0b8..8309f92c 100644 --- a/frontend/src/landing/components/LandingDifferentiators.tsx +++ b/frontend/src/landing/components/LandingDifferentiators.tsx @@ -1,64 +1,58 @@ const rows = [ - { feature: "Web-based dashboard", others: "Native Mac apps only" }, - { feature: "Open source (MIT)", others: "Closed source" }, - { feature: "Multi-agent (Claude, Codex, Aider, OpenCode)", others: "Single agent" }, - { feature: "Auto CI failure recovery", others: "Manual" }, - { feature: "Plugin architecture (7 slots)", others: "Fixed integrations" }, - { feature: "Git worktree isolation", others: "Shared workspace" }, + { feature: "Web-based dashboard", others: "Native Mac apps only" }, + { feature: "Open source (MIT)", others: "Closed source" }, + { feature: "Multi-agent (Claude, Codex, Aider, OpenCode)", others: "Single agent" }, + { feature: "Auto CI failure recovery", others: "Manual" }, + { feature: "Plugin architecture (7 slots)", others: "Fixed integrations" }, + { feature: "Git worktree isolation", others: "Shared workspace" }, ]; export function LandingDifferentiators() { - return ( -
-
-
- Why Agent Orchestrator -
-

- The only{" "} - open-source, web-based{" "} - agent orchestrator -

-

- Conductor, T3 Code, and Codex App are native Mac apps. AO runs in - your browser, works on any OS, and you can self-host or extend it. -

-
-
- - - - - - - - - - {rows.map((row, i) => ( - - - - - - ))} - -
- Feature - - AO - - Others -
- {row.feature} - - ✓ - - {row.others} -
-
-
- ); + return ( +
+
+
+ Why Agent Orchestrator +
+

+ The only open-source, web-based agent orchestrator +

+

+ Conductor, T3 Code, and Codex App are native Mac apps. AO runs in your browser, works on any OS, and you can + self-host or extend it. +

+
+
+ + + + + + + + + + {rows.map((row, i) => ( + + + + + + ))} + +
+ Feature + + AO + + Others +
{row.feature} + {row.others} +
+
+
+ ); } diff --git a/frontend/src/landing/components/LandingFeatures.tsx b/frontend/src/landing/components/LandingFeatures.tsx index a70530f1..0e1ab637 100644 --- a/frontend/src/landing/components/LandingFeatures.tsx +++ b/frontend/src/landing/components/LandingFeatures.tsx @@ -5,51 +5,51 @@ import { useEffect, useRef, useState } from "react"; type DemoKind = "parallel" | "recovery" | "plugins" | "dashboard"; const features: { n: string; title: string; desc: string; demo: DemoKind }[] = [ - { - n: "01", - title: "Multi-agent execution", - desc: "Run Claude Code, Codex, Cursor, Aider, and OpenCode in parallel. Each agent in its own git worktree, branch, and context.", - demo: "parallel", - }, - { - n: "02", - title: "Autonomous CI + review handling", - desc: "CI fails? The agent reads the logs and pushes a fix. Review comments land? The agent addresses them. You sleep, your agents ship.", - demo: "recovery", - }, - { - n: "03", - title: "Seven swappable slots", - desc: "Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal. Use tmux or process. GitHub or GitLab. Slack or webhooks.", - demo: "plugins", - }, - { - n: "04", - title: "Real-time Kanban + terminal", - desc: "Every agent's state in one view. Attach to any terminal via the browser. SSE updates every 5 seconds. WebSocket for live I/O.", - demo: "dashboard", - }, + { + n: "01", + title: "Multi-agent execution", + desc: "Run Claude Code, Codex, Cursor, Aider, and OpenCode in parallel. Each agent in its own git worktree, branch, and context.", + demo: "parallel", + }, + { + n: "02", + title: "Autonomous CI + review handling", + desc: "CI fails? The agent reads the logs and pushes a fix. Review comments land? The agent addresses them. You sleep, your agents ship.", + demo: "recovery", + }, + { + n: "03", + title: "Seven swappable slots", + desc: "Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal. Use tmux or process. GitHub or GitLab. Slack or webhooks.", + demo: "plugins", + }, + { + n: "04", + title: "Real-time Kanban + terminal", + desc: "Every agent's state in one view. Attach to any terminal via the browser. SSE updates every 5 seconds. WebSocket for live I/O.", + demo: "dashboard", + }, ]; // The feature's animated demo — the stacked back panel + a smaller front peek, // reused as-is from the original switcher so each card stays rich. function FeatureDemo({ kind }: { kind: DemoKind }) { - return ( -
-
- {kind === "parallel" && } - {kind === "recovery" && } - {kind === "plugins" && } - {kind === "dashboard" && } -
-
- {kind === "parallel" && } - {kind === "recovery" && } - {kind === "plugins" && } - {kind === "dashboard" && } -
-
- ); + return ( +
+
+ {kind === "parallel" && } + {kind === "recovery" && } + {kind === "plugins" && } + {kind === "dashboard" && } +
+
+ {kind === "parallel" && } + {kind === "recovery" && } + {kind === "plugins" && } + {kind === "dashboard" && } +
+
+ ); } // Sticky offset from the top of the viewport where each card pins (leaves room @@ -58,503 +58,463 @@ const BASE_TOP = 120; const STACK_GAP = 26; export function LandingFeatures() { - const cardRefs = useRef<(HTMLDivElement | null)[]>([]); - const [stack, setStack] = useState(false); + const cardRefs = useRef<(HTMLDivElement | null)[]>([]); + const [stack, setStack] = useState(false); - // Scroll-stack only on desktop; on narrow screens cards read as a plain list. - useEffect(() => { - const mq = window.matchMedia("(min-width: 768px)"); - const apply = () => setStack(mq.matches); - apply(); - mq.addEventListener("change", apply); - return () => mq.removeEventListener("change", apply); - }, []); + // Scroll-stack only on desktop; on narrow screens cards read as a plain list. + useEffect(() => { + const mq = window.matchMedia("(min-width: 768px)"); + const apply = () => setStack(mq.matches); + apply(); + mq.addEventListener("change", apply); + return () => mq.removeEventListener("change", apply); + }, []); - // As later cards pin on top, shrink + dim the cards beneath them so the deck - // reads as a stack. CSS transition smooths the steps; rAF throttles scroll. - useEffect(() => { - const els = cardRefs.current; - if (!stack) { - els.forEach((el) => { - if (el) { - el.style.transform = ""; - el.style.opacity = ""; - } - }); - return; - } - let raf = 0; - const update = () => { - raf = 0; - els.forEach((el, i) => { - if (!el) return; - let depth = 0; - for (let j = i + 1; j < els.length; j++) { - const ej = els[j]; - if (ej && ej.getBoundingClientRect().top <= BASE_TOP + j * STACK_GAP + 0.5) { - depth += 1; - } - } - el.style.transform = `scale(${1 - depth * 0.05})`; - el.style.opacity = `${Math.max(1 - depth * 0.16, 0.55)}`; - }); - }; - const onScroll = () => { - if (!raf) raf = requestAnimationFrame(update); - }; - update(); - window.addEventListener("scroll", onScroll, { passive: true }); - window.addEventListener("resize", onScroll); - return () => { - window.removeEventListener("scroll", onScroll); - window.removeEventListener("resize", onScroll); - if (raf) cancelAnimationFrame(raf); - }; - }, [stack]); + // As later cards pin on top, shrink + dim the cards beneath them so the deck + // reads as a stack. CSS transition smooths the steps; rAF throttles scroll. + useEffect(() => { + const els = cardRefs.current; + if (!stack) { + els.forEach((el) => { + if (el) { + el.style.transform = ""; + el.style.opacity = ""; + } + }); + return; + } + let raf = 0; + const update = () => { + raf = 0; + els.forEach((el, i) => { + if (!el) return; + let depth = 0; + for (let j = i + 1; j < els.length; j++) { + const ej = els[j]; + if (ej && ej.getBoundingClientRect().top <= BASE_TOP + j * STACK_GAP + 0.5) { + depth += 1; + } + } + el.style.transform = `scale(${1 - depth * 0.05})`; + el.style.opacity = `${Math.max(1 - depth * 0.16, 0.55)}`; + }); + }; + const onScroll = () => { + if (!raf) raf = requestAnimationFrame(update); + }; + update(); + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("resize", onScroll); + return () => { + window.removeEventListener("scroll", onScroll); + window.removeEventListener("resize", onScroll); + if (raf) cancelAnimationFrame(raf); + }; + }, [stack]); - return ( -
-
- - Features - -
+ return ( +
+
+ + Features + +
-

- A unified orchestrator that scales. -

+

+ A unified orchestrator that scales. +

-
- {features.map((f, i) => ( -
{ - cardRefs.current[i] = el; - }} - className="landing-card rounded-2xl grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-center overflow-hidden" - style={{ - padding: "clamp(1.5rem, 3vw, 2.5rem)", - marginBottom: "1.5rem", - transformOrigin: "center top", - transition: "transform 0.4s ease, opacity 0.4s ease, border-color 0.2s ease", - ...(stack - ? { position: "sticky", top: `${BASE_TOP + i * STACK_GAP}px`, zIndex: i + 1 } - : null), - }} - > -
-
- {f.n} -
-

- {f.title} -

-

- {f.desc} -

-
- -
- ))} -
-
- ); +
+ {features.map((f, i) => ( +
{ + cardRefs.current[i] = el; + }} + className="landing-card rounded-2xl grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-center overflow-hidden" + style={{ + padding: "clamp(1.5rem, 3vw, 2.5rem)", + marginBottom: "1.5rem", + transformOrigin: "center top", + transition: "transform 0.4s ease, opacity 0.4s ease, border-color 0.2s ease", + ...(stack ? { position: "sticky", top: `${BASE_TOP + i * STACK_GAP}px`, zIndex: i + 1 } : null), + }} + > +
+
+ {f.n} +
+

{f.title}

+

{f.desc}

+
+ +
+ ))} +
+
+ ); } /* ──────── 01 · Parallel ──────── */ function ParallelBack() { - const agents = [ - { name: "claude-code", task: "#42 auth", color: "rgba(255,159,102,0.7)", dur: 3.4, delay: 0 }, - { name: "codex", task: "#43 pagination", color: "rgba(134,239,172,0.65)", dur: 4.2, delay: 0.5 }, - { name: "aider", task: "#44 rate limit", color: "rgba(167,139,250,0.65)", dur: 3.6, delay: 1.0 }, - { name: "opencode", task: "#46 db refactor", color: "rgba(96,165,250,0.65)", dur: 4.8, delay: 0.3 }, - ]; - return ( -
-
- - 4 sessions · parallel - - - - live - -
-
- {agents.map((a) => ( -
-
- - - {a.name} - -
-
- {a.task} -
-
-
-
-
- ))} -
-
- ); + const agents = [ + { name: "claude-code", task: "#42 auth", color: "rgba(255,159,102,0.7)", dur: 3.4, delay: 0 }, + { name: "codex", task: "#43 pagination", color: "rgba(134,239,172,0.65)", dur: 4.2, delay: 0.5 }, + { name: "aider", task: "#44 rate limit", color: "rgba(167,139,250,0.65)", dur: 3.6, delay: 1.0 }, + { name: "opencode", task: "#46 db refactor", color: "rgba(96,165,250,0.65)", dur: 4.8, delay: 0.3 }, + ]; + return ( +
+
+ + 4 sessions · parallel + + + + live + +
+
+ {agents.map((a) => ( +
+
+ + {a.name} +
+
+ {a.task} +
+
+
+
+
+ ))} +
+
+ ); } function ParallelFront() { - const fleet = [ - { name: "claude-code", color: "rgba(255,159,102,0.85)" }, - { name: "codex", color: "rgba(134,239,172,0.75)" }, - { name: "aider", color: "rgba(167,139,250,0.75)" }, - { name: "opencode", color: "rgba(96,165,250,0.75)" }, - { name: "cursor", color: "rgba(244,114,182,0.65)" }, - ]; - return ( -
-
- Fleet · 5 agents -
-
- {fleet.map((a) => ( -
- - - {a.name} - -
- ))} -
-
- ); + const fleet = [ + { name: "claude-code", color: "rgba(255,159,102,0.85)" }, + { name: "codex", color: "rgba(134,239,172,0.75)" }, + { name: "aider", color: "rgba(167,139,250,0.75)" }, + { name: "opencode", color: "rgba(96,165,250,0.75)" }, + { name: "cursor", color: "rgba(244,114,182,0.65)" }, + ]; + return ( +
+
+ Fleet · 5 agents +
+
+ {fleet.map((a) => ( +
+ + {a.name} +
+ ))} +
+
+ ); } /* ──────── 02 · Recovery ──────── */ const recoveryStages: { time: string; text: string; kind: "info" | "fail" | "fix" | "ok" }[] = [ - { time: "10:42", text: "agent.spawn → s-312", kind: "info" }, - { time: "10:43", text: "✗ tests/auth failed", kind: "fail" }, - { time: "10:44", text: "agent.investigate()", kind: "info" }, - { time: "10:44", text: "patch · re-running ci", kind: "fix" }, - { time: "10:45", text: "✓ tests/auth (48/48)", kind: "ok" }, - { time: "10:45", text: "✗ lint failed", kind: "fail" }, - { time: "10:46", text: "patch · eslint --fix", kind: "fix" }, - { time: "10:47", text: "✓ lint passed", kind: "ok" }, - { time: "10:47", text: "● ready to merge", kind: "ok" }, + { time: "10:42", text: "agent.spawn → s-312", kind: "info" }, + { time: "10:43", text: "✗ tests/auth failed", kind: "fail" }, + { time: "10:44", text: "agent.investigate()", kind: "info" }, + { time: "10:44", text: "patch · re-running ci", kind: "fix" }, + { time: "10:45", text: "✓ tests/auth (48/48)", kind: "ok" }, + { time: "10:45", text: "✗ lint failed", kind: "fail" }, + { time: "10:46", text: "patch · eslint --fix", kind: "fix" }, + { time: "10:47", text: "✓ lint passed", kind: "ok" }, + { time: "10:47", text: "● ready to merge", kind: "ok" }, ]; function RecoveryBack() { - const [count, setCount] = useState(3); - useEffect(() => { - const id = setInterval(() => { - setCount((c) => (c >= recoveryStages.length ? 3 : c + 1)); - }, 1000); - return () => clearInterval(id); - }, []); - const visible = recoveryStages.slice(0, count); - return ( -
-
- PR #312 · feat/user-auth - - healing - -
-
- {visible.map((s, i) => { - const isLast = i === visible.length - 1; - const color = - s.kind === "fail" - ? "text-[rgba(248,113,113,0.85)]" - : s.kind === "ok" - ? "text-[rgba(134,239,172,0.85)]" - : s.kind === "fix" - ? "text-[rgba(251,191,36,0.85)]" - : "text-[var(--landing-muted)]"; - return ( -
- - {s.time} - - {s.text} -
- ); - })} -
-
- ); + const [count, setCount] = useState(3); + useEffect(() => { + const id = setInterval(() => { + setCount((c) => (c >= recoveryStages.length ? 3 : c + 1)); + }, 1000); + return () => clearInterval(id); + }, []); + const visible = recoveryStages.slice(0, count); + return ( +
+
+ PR #312 · feat/user-auth + healing +
+
+ {visible.map((s, i) => { + const isLast = i === visible.length - 1; + const color = + s.kind === "fail" + ? "text-[rgba(248,113,113,0.85)]" + : s.kind === "ok" + ? "text-[rgba(134,239,172,0.85)]" + : s.kind === "fix" + ? "text-[rgba(251,191,36,0.85)]" + : "text-[var(--landing-muted)]"; + return ( +
+ {s.time} + {s.text} +
+ ); + })} +
+
+ ); } function RecoveryFront() { - return ( -
-
- - before - - - 12/48 -
-
- - after - - - 48/48 -
-
- ); + return ( +
+
+ before + + 12/48 +
+
+ after + + 48/48 +
+
+ ); } /* ──────── 03 · Plugins ──────── */ function PluginsBack() { - const slots = [ - { slot: "agent", values: ["claude-code", "codex", "aider", "opencode"] }, - { slot: "tracker", values: ["github", "linear", "gitlab"] }, - { slot: "runtime", values: ["tmux", "process"] }, - { slot: "workspace", values: ["worktree", "clone"] }, - { slot: "scm", values: ["github", "gitlab"] }, - { slot: "notifier", values: ["slack", "webhook", "desktop"] }, - { slot: "terminal", values: ["iterm2", "web"] }, - ]; - const [tick, setTick] = useState(0); - useEffect(() => { - const id = setInterval(() => setTick((t) => t + 1), 1600); - return () => clearInterval(id); - }, []); - return ( -
-
- - agent-orchestrator.yaml - - - 7 slots - -
-
- {slots.map((s, i) => { - const val = s.values[(tick + i) % s.values.length]; - return ( -
- - {s.slot}: - - - {val} - -
- ); - })} -
-
- ); + const slots = [ + { slot: "agent", values: ["claude-code", "codex", "aider", "opencode"] }, + { slot: "tracker", values: ["github", "linear", "gitlab"] }, + { slot: "runtime", values: ["tmux", "process"] }, + { slot: "workspace", values: ["worktree", "clone"] }, + { slot: "scm", values: ["github", "gitlab"] }, + { slot: "notifier", values: ["slack", "webhook", "desktop"] }, + { slot: "terminal", values: ["iterm2", "web"] }, + ]; + const [tick, setTick] = useState(0); + useEffect(() => { + const id = setInterval(() => setTick((t) => t + 1), 1600); + return () => clearInterval(id); + }, []); + return ( +
+
+ agent-orchestrator.yaml + + 7 slots + +
+
+ {slots.map((s, i) => { + const val = s.values[(tick + i) % s.values.length]; + return ( +
+ {s.slot}: + + {val} + +
+ ); + })} +
+
+ ); } function PluginsFront() { - const pairs = [ - { from: "tmux", to: "process" }, - { from: "github", to: "linear" }, - { from: "slack", to: "webhook" }, - { from: "worktree", to: "clone" }, - ]; - const [idx, setIdx] = useState(0); - useEffect(() => { - const id = setInterval(() => setIdx((i) => (i + 1) % pairs.length), 1800); - return () => clearInterval(id); - }, []); - const p = pairs[idx]; - return ( -
- - swap - -
- - {p.from} - - - - {p.to} - -
-
- ); + const pairs = [ + { from: "tmux", to: "process" }, + { from: "github", to: "linear" }, + { from: "slack", to: "webhook" }, + { from: "worktree", to: "clone" }, + ]; + const [idx, setIdx] = useState(0); + useEffect(() => { + const id = setInterval(() => setIdx((i) => (i + 1) % pairs.length), 1800); + return () => clearInterval(id); + }, []); + const p = pairs[idx]; + return ( +
+ + swap + +
+ + {p.from} + + + + {p.to} + +
+
+ ); } /* ──────── 04 · Dashboard ──────── */ type KanbanCard = { - id: number; - col: 0 | 1 | 2; - title: string; - agent: string; - color: string; + id: number; + col: 0 | 1 | 2; + title: string; + agent: string; + color: string; }; function DashboardBack() { - const [cards, setCards] = useState([ - { id: 1, col: 0, title: "Add user auth", agent: "claude-code", color: "rgba(255,159,102,0.7)" }, - { id: 2, col: 0, title: "Fix pagination", agent: "codex", color: "rgba(134,239,172,0.65)" }, - { id: 3, col: 1, title: "Add rate limit", agent: "aider", color: "rgba(167,139,250,0.65)" }, - { id: 4, col: 2, title: "Refactor DB", agent: "opencode", color: "rgba(96,165,250,0.65)" }, - ]); - useEffect(() => { - const id = setInterval(() => { - setCards((prev) => { - const advanceable = prev.filter((c) => c.col < 2); - if (advanceable.length === 0) { - return prev.map((c) => ({ ...c, col: 0 as 0 | 1 | 2 })); - } - const oldest = advanceable[0]; - return prev.map((c) => - c.id === oldest.id ? { ...c, col: (c.col + 1) as 0 | 1 | 2 } : c, - ); - }); - }, 2400); - return () => clearInterval(id); - }, []); - const cols = ["Working", "Review", "Merged"]; - return ( -
-
- - my-saas-app · 4 sessions - - - - sse - -
-
- {cols.map((name, col) => ( -
-
- {name} -
- {cards - .filter((c) => c.col === col) - .map((c) => ( -
-
{c.title}
-
- - - {c.agent} - -
-
- ))} -
- ))} -
-
- ); + const [cards, setCards] = useState([ + { id: 1, col: 0, title: "Add user auth", agent: "claude-code", color: "rgba(255,159,102,0.7)" }, + { id: 2, col: 0, title: "Fix pagination", agent: "codex", color: "rgba(134,239,172,0.65)" }, + { id: 3, col: 1, title: "Add rate limit", agent: "aider", color: "rgba(167,139,250,0.65)" }, + { id: 4, col: 2, title: "Refactor DB", agent: "opencode", color: "rgba(96,165,250,0.65)" }, + ]); + useEffect(() => { + const id = setInterval(() => { + setCards((prev) => { + const advanceable = prev.filter((c) => c.col < 2); + if (advanceable.length === 0) { + return prev.map((c) => ({ ...c, col: 0 as 0 | 1 | 2 })); + } + const oldest = advanceable[0]; + return prev.map((c) => (c.id === oldest.id ? { ...c, col: (c.col + 1) as 0 | 1 | 2 } : c)); + }); + }, 2400); + return () => clearInterval(id); + }, []); + const cols = ["Working", "Review", "Merged"]; + return ( +
+
+ my-saas-app · 4 sessions + + + sse + +
+
+ {cols.map((name, col) => ( +
+
+ {name} +
+ {cards + .filter((c) => c.col === col) + .map((c) => ( +
+
{c.title}
+
+ + {c.agent} +
+
+ ))} +
+ ))} +
+
+ ); } const streamPool = [ - "tests/auth.py::test_login", - "tests/api.py::test_pagination", - "tests/db.py::test_migration", - "tests/queue.py::test_dequeue", - "tests/auth.py::test_logout", - "tests/api.py::test_cursor", - "tests/db.py::test_index", - "tests/queue.py::test_retry", + "tests/auth.py::test_login", + "tests/api.py::test_pagination", + "tests/db.py::test_migration", + "tests/queue.py::test_dequeue", + "tests/auth.py::test_logout", + "tests/api.py::test_cursor", + "tests/db.py::test_index", + "tests/queue.py::test_retry", ]; function DashboardFront() { - const [stream, setStream] = useState(() => - streamPool.slice(0, 4).map((text, i) => ({ id: i, text, exiting: false })), - ); - const nextRef = useRef(4); - useEffect(() => { - const id = setInterval(() => { - setStream((prev) => { - const marked = prev.map((l, i) => (i === 0 ? { ...l, exiting: true } : l)); - const next = [ - ...marked, - { - id: nextRef.current, - text: streamPool[nextRef.current % streamPool.length], - exiting: false, - }, - ]; - nextRef.current += 1; - return next; - }); - setTimeout(() => { - setStream((prev) => prev.filter((l) => !l.exiting)); - }, 240); - }, 1300); - return () => clearInterval(id); - }, []); - return ( -
-
- s-003 · attached - tail -f -
-
- {stream.map((l) => ( -
- {" "} - {l.text} -
- ))} -
-
- ); + const [stream, setStream] = useState(() => + streamPool.slice(0, 4).map((text, i) => ({ id: i, text, exiting: false })), + ); + const nextRef = useRef(4); + useEffect(() => { + const id = setInterval(() => { + setStream((prev) => { + const marked = prev.map((l, i) => (i === 0 ? { ...l, exiting: true } : l)); + const next = [ + ...marked, + { + id: nextRef.current, + text: streamPool[nextRef.current % streamPool.length], + exiting: false, + }, + ]; + nextRef.current += 1; + return next; + }); + setTimeout(() => { + setStream((prev) => prev.filter((l) => !l.exiting)); + }, 240); + }, 1300); + return () => clearInterval(id); + }, []); + return ( +
+
+ s-003 · attached + tail -f +
+
+ {stream.map((l) => ( +
+ {l.text} +
+ ))} +
+
+ ); } diff --git a/frontend/src/landing/components/LandingHero.tsx b/frontend/src/landing/components/LandingHero.tsx index ab820a04..32aa1bec 100644 --- a/frontend/src/landing/components/LandingHero.tsx +++ b/frontend/src/landing/components/LandingHero.tsx @@ -1,102 +1,101 @@ interface LandingHeroProps { - starsLabel: string; + starsLabel: string; } export function LandingHero({ starsLabel }: LandingHeroProps) { - return ( -
-
-
- - Open Source · MIT Licensed · {starsLabel} GitHub Stars -
-

- Run 30 AI agents in parallel. -
- One dashboard. -

-

- Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode - in isolated git worktrees. Each agent gets its own branch, creates PRs, - fixes CI, and addresses reviews autonomously. -

-
-
- $ npx @aoagents/ao start -
- - Read Docs - - - View on GitHub - -
+ return ( +
+
+
+ + Open Source · MIT Licensed · {starsLabel} GitHub Stars +
+

+ Run 30 AI agents in parallel. +
+ One dashboard. +

+

+ Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode in isolated git worktrees. Each + agent gets its own branch, creates PRs, fixes CI, and addresses reviews autonomously. +

+
+
+ $ npx @aoagents/ao start +
+ + Read Docs + + + View on GitHub + +
-
-
- {/* Laptop screen / lid */} -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - Agent Orchestrator dashboard — live agent sessions flowing from work to review to merge -
-
- {/* Laptop base / hinge */} -
-
-
-
-
-
-
-
-
- ); +
+
+ {/* Laptop screen / lid */} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Agent Orchestrator dashboard — live agent sessions flowing from work to review to merge +
+
+ {/* Laptop base / hinge */} +
+
+
+
+
+
+
+
+
+ ); } diff --git a/frontend/src/landing/components/LandingHowItWorks.tsx b/frontend/src/landing/components/LandingHowItWorks.tsx index 77f785a0..670646a8 100644 --- a/frontend/src/landing/components/LandingHowItWorks.tsx +++ b/frontend/src/landing/components/LandingHowItWorks.tsx @@ -5,312 +5,315 @@ import { useEffect, useRef, useState } from "react"; const DURATION_MS = 3000; const steps = [ - { - n: "01", - title: "Configure & assign", - titleEm: "assign", - desc: "Point Agent Orchestrator at your repo with a YAML config. Choose your agent, set up trackers and notifiers. One file, full control.", - tags: ["YAML", "Plugins", "Trackers"], - kind: "cli" as const, - }, - { - n: "02", - title: "Agents work", - titleEm: "work", - desc: "Each agent spawns in an isolated worktree. They write code, create PRs, run tests, and fix failures. Monitor everything from the live dashboard, or let them run.", - tags: ["Worktrees", "Live dashboard", "Parallel"], - kind: "dashboard" as const, - }, - { - n: "03", - title: "PRs land", - titleEm: "land", - desc: "Agents create pull requests, address review comments, fix CI failures, and get them to mergeable state. Your morning starts with merged PRs, not a backlog.", - tags: ["Pull requests", "CI fixes", "Review"], - kind: "prs" as const, - }, + { + n: "01", + title: "Configure & assign", + titleEm: "assign", + desc: "Point Agent Orchestrator at your repo with a YAML config. Choose your agent, set up trackers and notifiers. One file, full control.", + tags: ["YAML", "Plugins", "Trackers"], + kind: "cli" as const, + }, + { + n: "02", + title: "Agents work", + titleEm: "work", + desc: "Each agent spawns in an isolated worktree. They write code, create PRs, run tests, and fix failures. Monitor everything from the live dashboard, or let them run.", + tags: ["Worktrees", "Live dashboard", "Parallel"], + kind: "dashboard" as const, + }, + { + n: "03", + title: "PRs land", + titleEm: "land", + desc: "Agents create pull requests, address review comments, fix CI failures, and get them to mergeable state. Your morning starts with merged PRs, not a backlog.", + tags: ["Pull requests", "CI fixes", "Review"], + kind: "prs" as const, + }, ]; export function LandingHowItWorks() { - const [active, setActive] = useState(0); - const [progress, setProgress] = useState(0); - const [isDesktop, setIsDesktop] = useState(true); - const pausedRef = useRef(false); - const startRef = useRef(null); + const [active, setActive] = useState(0); + const [progress, setProgress] = useState(0); + const [isDesktop, setIsDesktop] = useState(true); + const pausedRef = useRef(false); + const startRef = useRef(null); - useEffect(() => { - const mq = window.matchMedia("(min-width: 768px)"); - const apply = () => setIsDesktop(mq.matches); - apply(); - mq.addEventListener("change", apply); - return () => mq.removeEventListener("change", apply); - }, []); + useEffect(() => { + const mq = window.matchMedia("(min-width: 768px)"); + const apply = () => setIsDesktop(mq.matches); + apply(); + mq.addEventListener("change", apply); + return () => mq.removeEventListener("change", apply); + }, []); - useEffect(() => { - let raf = 0; - const tick = (now: number) => { - if (startRef.current === null) startRef.current = now; - if (!pausedRef.current) { - const p = Math.min((now - startRef.current) / DURATION_MS, 1); - setProgress(p); - if (p >= 1) { - startRef.current = now; - setActive((a) => (a + 1) % steps.length); - setProgress(0); - } - } else { - startRef.current = now - progress * DURATION_MS; - } - raf = requestAnimationFrame(tick); - }; - raf = requestAnimationFrame(tick); - return () => cancelAnimationFrame(raf); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [active]); + useEffect(() => { + let raf = 0; + const tick = (now: number) => { + if (startRef.current === null) startRef.current = now; + if (!pausedRef.current) { + const p = Math.min((now - startRef.current) / DURATION_MS, 1); + setProgress(p); + if (p >= 1) { + startRef.current = now; + setActive((a) => (a + 1) % steps.length); + setProgress(0); + } + } else { + startRef.current = now - progress * DURATION_MS; + } + raf = requestAnimationFrame(tick); + }; + raf = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [active]); - const select = (i: number) => { - if (i === active) return; - startRef.current = null; - setProgress(0); - setActive(i); - }; + const select = (i: number) => { + if (i === active) return; + startRef.current = null; + setProgress(0); + setActive(i); + }; - return ( -
-
-
- Process -
-

- Three steps to{" "} - orchestration -

-
+ return ( +
+
+
Process
+

+ Three steps to orchestration +

+
-
(pausedRef.current = true)} - onMouseLeave={() => (pausedRef.current = false)} - > - {steps.map((step, i) => { - const isActive = i === active; - return ( -
select(i)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - select(i); - } - }} - className="relative min-w-0 cursor-pointer overflow-hidden border-l border-[var(--landing-border-subtle)] pl-7 pr-5 py-2 first:border-l-0 first:pl-0 md:first:pl-7" - style={{ - flex: isDesktop - ? isActive - ? "1 1 0%" - : "0 1 15rem" - : "0 0 auto", - transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)", - }} - > - {/* Header — always visible */} -
- {step.n} -
-

- {step.title.replace(` ${step.titleEm}`, "")}{" "} - - {step.titleEm} - -

+
(pausedRef.current = true)} + onMouseLeave={() => (pausedRef.current = false)} + > + {steps.map((step, i) => { + const isActive = i === active; + return ( +
select(i)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + select(i); + } + }} + className="relative min-w-0 cursor-pointer overflow-hidden border-l border-[var(--landing-border-subtle)] pl-7 pr-5 py-2 first:border-l-0 first:pl-0 md:first:pl-7" + style={{ + flex: isDesktop ? (isActive ? "1 1 0%" : "0 1 15rem") : "0 0 auto", + transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)", + }} + > + {/* Header — always visible */} +
+ {step.n} +
+

+ {step.title.replace(` ${step.titleEm}`, "")}{" "} + {step.titleEm} +

- {/* Expanding body */} -
-
- {/* Vertical progress bar */} -
-
-
+ {/* Expanding body */} +
+
+ {/* Vertical progress bar */} +
+
+
-
-

- {step.desc} -

-
- {step.kind === "cli" && } - {step.kind === "dashboard" && } - {step.kind === "prs" && } -
-
- {step.tags.map((t, ti) => ( - - {ti > 0 && ·} - {t} - - ))} -
-
-
-
-
- ); - })} -
-
- ); +
+

+ {step.desc} +

+
+ {step.kind === "cli" && } + {step.kind === "dashboard" && } + {step.kind === "prs" && } +
+
+ {step.tags.map((t, ti) => ( + + {ti > 0 && ·} + {t} + + ))} +
+
+
+
+ + ); + })} + + + ); } function CliDemo() { - return ( -
-
-
-
-
-
-
-
- ${" "} - ao batch-spawn 42 43 44 45 46 -
-
 
-
⟡ Loading config from agent-orchestrator.yaml
-
⟡ Resolving 5 issues from GitHub
-
⟡ Spawning sessions in worktrees...
-
✓ Session s-001 spawned → issue #42
-
✓ Session s-002 spawned → issue #43
-
✓ Session s-003 spawned → issue #44
-
✓ Session s-004 spawned → issue #45
-
✓ Session s-005 spawned → issue #46
-
 
-
- - 5 agents working · Dashboard → http://localhost:3000 -
-
-
- ); + return ( +
+
+
+
+
+
+
+
+ ${" "} + ao batch-spawn 42 43 44 45 46 +
+
 
+
⟡ Loading config from agent-orchestrator.yaml
+
⟡ Resolving 5 issues from GitHub
+
⟡ Spawning sessions in worktrees...
+
✓ Session s-001 spawned → issue #42
+
✓ Session s-002 spawned → issue #43
+
✓ Session s-003 spawned → issue #44
+
✓ Session s-004 spawned → issue #45
+
✓ Session s-005 spawned → issue #46
+
 
+
+ + + 5 agents working · Dashboard → http://localhost:3000 + +
+
+
+ ); } function DashboardDemo() { - return ( -
-
-
-
-
- my-saas-app · 5 sessions -
-
- - - - -
-
- ); + return ( +
+
+
+
+
+ my-saas-app · 5 sessions +
+
+ + + + +
+
+ ); } function PrsDemo() { - return ( -
- {[ - { branch: "feat/user-auth", title: "Add user authentication flow" }, - { branch: "fix/pagination-offset", title: "Fix off-by-one in cursor pagination" }, - { branch: "feat/rate-limiting", title: "Add Redis-backed rate limiter" }, - { branch: "refactor/db-layer", title: "Extract repository pattern from services" }, - ].map((pr) => ( -
-
-
{pr.branch}
-
{pr.title}
-
-
- ✓ Merged -
-
- ))} -
- ); + return ( +
+ {[ + { branch: "feat/user-auth", title: "Add user authentication flow" }, + { branch: "fix/pagination-offset", title: "Fix off-by-one in cursor pagination" }, + { branch: "feat/rate-limiting", title: "Add Redis-backed rate limiter" }, + { branch: "refactor/db-layer", title: "Extract repository pattern from services" }, + ].map((pr) => ( +
+
+
{pr.branch}
+
{pr.title}
+
+
+ ✓ Merged +
+
+ ))} +
+ ); } interface DashCardData { - title: string; - meta: string; - agent: string; - amber?: boolean; - done?: boolean; + title: string; + meta: string; + agent: string; + amber?: boolean; + done?: boolean; } function DashColumn({ title, cards }: { title: string; cards: DashCardData[] }) { - return ( -
-
- {title} -
- {cards.map((card) => ( -
-
{card.title}
-
{card.meta}
-
- {card.done ? ( - - ) : ( - - )} - {card.agent} -
-
- ))} -
- ); + return ( +
+
+ {title} +
+ {cards.map((card) => ( +
+
{card.title}
+
{card.meta}
+
+ {card.done ? ( + + ) : ( + + )} + {card.agent} +
+
+ ))} +
+ ); } diff --git a/frontend/src/landing/components/LandingNav.tsx b/frontend/src/landing/components/LandingNav.tsx index b9ed3b2e..4288d70b 100644 --- a/frontend/src/landing/components/LandingNav.tsx +++ b/frontend/src/landing/components/LandingNav.tsx @@ -1,85 +1,94 @@ "use client"; function XIcon() { - return ( - - ); + return ( + + ); } function DiscordIcon() { - return ( - - ); + return ( + + ); } function GithubIcon() { - return ( - - ); + return ( + + ); } export function LandingNav() { - return ( - - ); + return ( + + ); } diff --git a/frontend/src/landing/components/LandingQuickStart.tsx b/frontend/src/landing/components/LandingQuickStart.tsx index cd8b60b4..65155995 100644 --- a/frontend/src/landing/components/LandingQuickStart.tsx +++ b/frontend/src/landing/components/LandingQuickStart.tsx @@ -1,44 +1,52 @@ const steps = [ - { num: "STEP 01", title: "Install", desc: "One command. No dependencies beyond Node.js.", cmd: "npm i -g @aoagents/ao" }, - { num: "STEP 02", title: "Configure", desc: "Create an agent-orchestrator.yaml. Pick your agents, tracker, and notifiers.", cmd: "ao start" }, - { num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" }, + { + num: "STEP 01", + title: "Install", + desc: "One command. No dependencies beyond Node.js.", + cmd: "npm i -g @aoagents/ao", + }, + { + num: "STEP 02", + title: "Configure", + desc: "Create an agent-orchestrator.yaml. Pick your agents, tracker, and notifiers.", + cmd: "ao start", + }, + { num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" }, ]; export function LandingQuickStart() { - return ( -
-
-
- Get started in 60 seconds -
-

- Three commands to{" "} - launch -

-
-
- {steps.map((s) => ( -
-
- {s.num} -
-

- {s.title} -

-

- {s.desc} -

-
- $ {s.cmd} -
-
- ))} -
- -
- ); + return ( +
+
+
+ Get started in 60 seconds +
+

+ Three commands to launch +

+
+
+ {steps.map((s) => ( +
+
+ {s.num} +
+

{s.title}

+

{s.desc}

+
+ $ {s.cmd} +
+
+ ))} +
+ +
+ ); } diff --git a/frontend/src/landing/components/LandingStats.tsx b/frontend/src/landing/components/LandingStats.tsx index 823ba517..c2fab551 100644 --- a/frontend/src/landing/components/LandingStats.tsx +++ b/frontend/src/landing/components/LandingStats.tsx @@ -1,51 +1,46 @@ import type { GitHubRepoStats } from "@/lib/github-repo"; interface LandingStatsProps { - stats: GitHubRepoStats; + stats: GitHubRepoStats; } export function LandingStats({ stats }: LandingStatsProps) { - const cards = [ - { number: stats.stars.toLocaleString(), label: "GitHub Stars" }, - { number: stats.forks.toLocaleString(), label: "Forks" }, - { number: stats.openIssues.toLocaleString(), label: "Open Issues" }, - { number: stats.watchers.toLocaleString(), label: "Watchers" }, - ]; + const cards = [ + { number: stats.stars.toLocaleString(), label: "GitHub Stars" }, + { number: stats.forks.toLocaleString(), label: "Forks" }, + { number: stats.openIssues.toLocaleString(), label: "Open Issues" }, + { number: stats.watchers.toLocaleString(), label: "Watchers" }, + ]; - return ( -
-
- {cards.map((stat) => ( -
-
- {stat.number} -
-
- {stat.label} -
-
- ))} -
-
- - - {stats.stars.toLocaleString()} - stars on GitHub - -
-
- - Built with itself — this repo is managed by Agent Orchestrator -
-
-
- ); + return ( +
+
+ {cards.map((stat) => ( +
+
+ {stat.number} +
+
{stat.label}
+
+ ))} +
+
+ + + {stats.stars.toLocaleString()} + stars on GitHub + +
+
+ + Built with itself — this repo is managed by Agent Orchestrator +
+
+
+ ); } diff --git a/frontend/src/landing/components/LandingTestimonials.tsx b/frontend/src/landing/components/LandingTestimonials.tsx index a555bbae..982e7c41 100644 --- a/frontend/src/landing/components/LandingTestimonials.tsx +++ b/frontend/src/landing/components/LandingTestimonials.tsx @@ -3,170 +3,155 @@ import { useEffect, useState } from "react"; const testimonials = [ - { - quote: - "Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.", - img: "https://i.pravatar.cc/120?img=13", - name: "Staff Engineer", - role: "Series B Startup", - }, - { - quote: - "The auto CI recovery alone saves me hours a week. Agents fix their own broken tests. I just review and merge.", - img: "https://i.pravatar.cc/120?img=32", - name: "Solo Founder", - role: "Indie SaaS", - }, - { - quote: - "We went from 3 PRs/day to 15 PRs/day. The plugin system means we swapped in GitLab and Linear without changing our workflow.", - img: "https://i.pravatar.cc/120?img=8", - name: "Eng Lead", - role: "20-person team", - }, + { + quote: "Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.", + img: "https://i.pravatar.cc/120?img=13", + name: "Staff Engineer", + role: "Series B Startup", + }, + { + quote: + "The auto CI recovery alone saves me hours a week. Agents fix their own broken tests. I just review and merge.", + img: "https://i.pravatar.cc/120?img=32", + name: "Solo Founder", + role: "Indie SaaS", + }, + { + quote: + "We went from 3 PRs/day to 15 PRs/day. The plugin system means we swapped in GitLab and Linear without changing our workflow.", + img: "https://i.pravatar.cc/120?img=8", + name: "Eng Lead", + role: "20-person team", + }, ]; const ROTATE_MS = 5500; export function LandingTestimonials() { - const [active, setActive] = useState(0); - const [show, setShow] = useState(true); - const [paused, setPaused] = useState(false); + const [active, setActive] = useState(0); + const [show, setShow] = useState(true); + const [paused, setPaused] = useState(false); - const change = (next: number) => { - if (next === active) return; - setShow(false); - window.setTimeout(() => { - setActive(next); - setShow(true); - }, 240); - }; + const change = (next: number) => { + if (next === active) return; + setShow(false); + window.setTimeout(() => { + setActive(next); + setShow(true); + }, 240); + }; - useEffect(() => { - if (paused) return; - const t = window.setTimeout( - () => change((active + 1) % testimonials.length), - ROTATE_MS, - ); - return () => window.clearTimeout(t); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [active, paused]); + useEffect(() => { + if (paused) return; + const t = window.setTimeout(() => change((active + 1) % testimonials.length), ROTATE_MS); + return () => window.clearTimeout(t); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [active, paused]); - const t = testimonials[active]; + const t = testimonials[active]; - return ( -
-
-
- What engineers say -
-

- Trusted by builders -

-
+ return ( +
+
+
+ What engineers say +
+

+ Trusted by builders +

+
-
setPaused(true)} - onMouseLeave={() => setPaused(false)} - > - {/* Quote — fades on change */} -
-
- “{t.quote}” -
-
+
setPaused(true)} onMouseLeave={() => setPaused(false)}> + {/* Quote — fades on change */} +
+
+ “{t.quote}” +
+
- {/* Bottom row — author cluster on the left, step counter on the right */} -
-
-
- {testimonials.map((item, i) => { - const isActive = i === active; - const size = isActive ? 56 : 44; - return ( - - ); - })} -
+ {/* Bottom row — author cluster on the left, step counter on the right */} +
+
+
+ {testimonials.map((item, i) => { + const isActive = i === active; + const size = isActive ? 56 : 44; + return ( + + ); + })} +
- {/* Vertical divider */} -
+ {/* Vertical divider */} +
- {/* Author — fades on change */} -
-
- {t.name} -
-
- {t.role} -
-
-
+ {/* Author — fades on change */} +
+
{t.name}
+
{t.role}
+
+
- {/* Step counter — fills the right side */} -
- - {String(active + 1).padStart(2, "0")} - - - / {String(testimonials.length).padStart(2, "0")} - -
-
-
-
- ); + {/* Step counter — fills the right side */} +
+ + {String(active + 1).padStart(2, "0")} + + + / {String(testimonials.length).padStart(2, "0")} + +
+
+
+ + ); } diff --git a/frontend/src/landing/components/LandingUseCases.tsx b/frontend/src/landing/components/LandingUseCases.tsx index 9107aa21..d3de93ae 100644 --- a/frontend/src/landing/components/LandingUseCases.tsx +++ b/frontend/src/landing/components/LandingUseCases.tsx @@ -7,64 +7,64 @@ const fg = "text-[var(--landing-fg)]/80"; const ok = "text-[rgba(134,239,172,0.8)]"; type UseCase = { - eyebrow: string; - title: string; - desc: string; - prefix: "$" | "⟡"; - cmd: string; - outcome: string; + eyebrow: string; + title: string; + desc: string; + prefix: "$" | "⟡"; + cmd: string; + outcome: string; }; // Real, grounded use cases — real ao commands, reaction keys, and lifecycle states. const cases: UseCase[] = [ - { - eyebrow: "Backlog", - title: "Clear it overnight", - desc: "One agent per issue, each in its own git worktree, all running at once.", - prefix: "$", - cmd: "ao batch-spawn 142 143 144 145", - outcome: "4 worktrees · 4 PRs", - }, - { - eyebrow: "CI recovery", - title: "Self-healing builds", - desc: "A check goes red; the agent reads the logs, pushes a fix, and waits for green.", - prefix: "⟡", - cmd: "reaction · ci-failed", - outcome: "ci_failed → mergeable", - }, - { - eyebrow: "Review loop", - title: "Answers its own reviews", - desc: "Comments land; the agent addresses each one and re-requests review.", - prefix: "⟡", - cmd: "reaction · changes-requested", - outcome: "changes_requested → approved", - }, - { - eyebrow: "Migration", - title: "Grinds through the long ones", - desc: "Hand one agent a sweeping change and let it work file by file until tests pass.", - prefix: "$", - cmd: "ao spawn 305 --agent claude-code", - outcome: "23 files · tests green", - }, - { - eyebrow: "Per-role", - title: "Right model per job", - desc: "Claude Code orchestrates, Codex does the work. Pick the tool per task.", - prefix: "$", - cmd: "ao spawn 88 --agent codex", - outcome: "codex #88 · claude-code #91", - }, - { - eyebrow: "Multi-project", - title: "Every repo, one screen", - desc: "Register all your repos and supervise their agents from a single dashboard.", - prefix: "$", - cmd: "ao start", - outcome: "3 projects · one dashboard", - }, + { + eyebrow: "Backlog", + title: "Clear it overnight", + desc: "One agent per issue, each in its own git worktree, all running at once.", + prefix: "$", + cmd: "ao batch-spawn 142 143 144 145", + outcome: "4 worktrees · 4 PRs", + }, + { + eyebrow: "CI recovery", + title: "Self-healing builds", + desc: "A check goes red; the agent reads the logs, pushes a fix, and waits for green.", + prefix: "⟡", + cmd: "reaction · ci-failed", + outcome: "ci_failed → mergeable", + }, + { + eyebrow: "Review loop", + title: "Answers its own reviews", + desc: "Comments land; the agent addresses each one and re-requests review.", + prefix: "⟡", + cmd: "reaction · changes-requested", + outcome: "changes_requested → approved", + }, + { + eyebrow: "Migration", + title: "Grinds through the long ones", + desc: "Hand one agent a sweeping change and let it work file by file until tests pass.", + prefix: "$", + cmd: "ao spawn 305 --agent claude-code", + outcome: "23 files · tests green", + }, + { + eyebrow: "Per-role", + title: "Right model per job", + desc: "Claude Code orchestrates, Codex does the work. Pick the tool per task.", + prefix: "$", + cmd: "ao spawn 88 --agent codex", + outcome: "codex #88 · claude-code #91", + }, + { + eyebrow: "Multi-project", + title: "Every repo, one screen", + desc: "Register all your repos and supervise their agents from a single dashboard.", + prefix: "$", + cmd: "ao start", + outcome: "3 projects · one dashboard", + }, ]; const N = cases.length; @@ -74,162 +74,153 @@ const CARD_W = 360; const CARD_H = 440; export function LandingUseCases() { - const viewportRef = useRef(null); - const ringRef = useRef(null); - const cardRefs = useRef<(HTMLDivElement | null)[]>([]); + const viewportRef = useRef(null); + const ringRef = useRef(null); + const cardRefs = useRef<(HTMLDivElement | null)[]>([]); - const angle = useRef(0); - const dragging = useRef(false); - const paused = useRef(false); - const reduced = useRef(false); - const start = useRef({ x: 0, a: 0 }); + const angle = useRef(0); + const dragging = useRef(false); + const paused = useRef(false); + const reduced = useRef(false); + const start = useRef({ x: 0, a: 0 }); - // rAF loop — rotate the ring and fade/scale each card by how far it faces the - // camera. Imperative (no setState) so 60fps stays smooth and re-render-free. - useEffect(() => { - reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches; - let raf = 0; - const loop = () => { - if (!dragging.current && !paused.current && !reduced.current) { - angle.current += 0.12; - } - const a = angle.current; - if (ringRef.current) { - ringRef.current.style.transform = `translateZ(-${RADIUS}px) rotateY(${a}deg)`; - } - cardRefs.current.forEach((el, i) => { - if (!el) return; - const facing = Math.cos(((i * THETA + a) * Math.PI) / 180); - const vis = Math.max(facing, 0); - el.style.opacity = `${0.2 + 0.8 * vis}`; - el.style.transform = `rotateY(${i * THETA}deg) translateZ(${RADIUS}px) scale(${0.9 + 0.1 * vis})`; - }); - raf = requestAnimationFrame(loop); - }; - raf = requestAnimationFrame(loop); - return () => cancelAnimationFrame(raf); - }, []); + // rAF loop — rotate the ring and fade/scale each card by how far it faces the + // camera. Imperative (no setState) so 60fps stays smooth and re-render-free. + useEffect(() => { + reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + let raf = 0; + const loop = () => { + if (!dragging.current && !paused.current && !reduced.current) { + angle.current += 0.12; + } + const a = angle.current; + if (ringRef.current) { + ringRef.current.style.transform = `translateZ(-${RADIUS}px) rotateY(${a}deg)`; + } + cardRefs.current.forEach((el, i) => { + if (!el) return; + const facing = Math.cos(((i * THETA + a) * Math.PI) / 180); + const vis = Math.max(facing, 0); + el.style.opacity = `${0.2 + 0.8 * vis}`; + el.style.transform = `rotateY(${i * THETA}deg) translateZ(${RADIUS}px) scale(${0.9 + 0.1 * vis})`; + }); + raf = requestAnimationFrame(loop); + }; + raf = requestAnimationFrame(loop); + return () => cancelAnimationFrame(raf); + }, []); - const onPointerDown = (e: ReactPointerEvent) => { - dragging.current = true; - start.current = { x: e.clientX, a: angle.current }; - e.currentTarget.setPointerCapture(e.pointerId); - if (viewportRef.current) viewportRef.current.style.cursor = "grabbing"; - }; - const onPointerMove = (e: ReactPointerEvent) => { - if (!dragging.current) return; - angle.current = start.current.a + (e.clientX - start.current.x) * 0.4; - }; - const onPointerUp = () => { - dragging.current = false; - if (viewportRef.current) viewportRef.current.style.cursor = "grab"; - }; + const onPointerDown = (e: ReactPointerEvent) => { + dragging.current = true; + start.current = { x: e.clientX, a: angle.current }; + e.currentTarget.setPointerCapture(e.pointerId); + if (viewportRef.current) viewportRef.current.style.cursor = "grabbing"; + }; + const onPointerMove = (e: ReactPointerEvent) => { + if (!dragging.current) return; + angle.current = start.current.a + (e.clientX - start.current.x) * 0.4; + }; + const onPointerUp = () => { + dragging.current = false; + if (viewportRef.current) viewportRef.current.style.cursor = "grab"; + }; - return ( -
-
-
- Use cases -
-

- One orchestrator, many jobs -

-

- Point AO at the work and walk away — drag to explore what a single - run can do. -

-
+ return ( +
+
+
+ Use cases +
+

+ One orchestrator, many jobs +

+

+ Point AO at the work and walk away — drag to explore what a single run can do. +

+
-
(paused.current = true)} - onMouseLeave={() => { - paused.current = false; - onPointerUp(); - }} - onPointerDown={onPointerDown} - onPointerMove={onPointerMove} - onPointerUp={onPointerUp} - className="landing-reveal relative mx-auto select-none" - style={{ - perspective: "1900px", - height: `${CARD_H + 80}px`, - maxWidth: "1120px", - cursor: "grab", - touchAction: "pan-y", - WebkitMaskImage: - "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)", - maskImage: - "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)", - }} - > -
- {cases.map((c, i) => ( -
{ - cardRefs.current[i] = el; - }} - style={{ - position: "absolute", - left: "50%", - top: "50%", - width: `${CARD_W}px`, - height: `${CARD_H}px`, - marginLeft: `-${CARD_W / 2}px`, - marginTop: `-${CARD_H / 2}px`, - backfaceVisibility: "hidden", - }} - > -
-
- {c.eyebrow} -
-

- {c.title} -

-

- {c.desc} -

-
-
- {c.prefix}{" "} - {c.cmd} -
-
- → {c.outcome} -
-
-
-
- ))} -
-
-
- ); +
(paused.current = true)} + onMouseLeave={() => { + paused.current = false; + onPointerUp(); + }} + onPointerDown={onPointerDown} + onPointerMove={onPointerMove} + onPointerUp={onPointerUp} + className="landing-reveal relative mx-auto select-none" + style={{ + perspective: "1900px", + height: `${CARD_H + 80}px`, + maxWidth: "1120px", + cursor: "grab", + touchAction: "pan-y", + WebkitMaskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)", + maskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)", + }} + > +
+ {cases.map((c, i) => ( +
{ + cardRefs.current[i] = el; + }} + style={{ + position: "absolute", + left: "50%", + top: "50%", + width: `${CARD_W}px`, + height: `${CARD_H}px`, + marginLeft: `-${CARD_W / 2}px`, + marginTop: `-${CARD_H / 2}px`, + backfaceVisibility: "hidden", + }} + > +
+
+ {c.eyebrow} +
+

+ {c.title} +

+

+ {c.desc} +

+
+
+ {c.prefix} {c.cmd} +
+
→ {c.outcome}
+
+
+
+ ))} +
+
+
+ ); } diff --git a/frontend/src/landing/components/LandingVideo.tsx b/frontend/src/landing/components/LandingVideo.tsx index 07058074..132537b6 100644 --- a/frontend/src/landing/components/LandingVideo.tsx +++ b/frontend/src/landing/components/LandingVideo.tsx @@ -1,20 +1,20 @@ export function LandingVideo() { - return ( -
-
- - See it in action - -
-
-