merge: dev into main
This commit is contained in:
commit
91833198c7
223 changed files with 10703 additions and 1731 deletions
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
|
|
@ -411,10 +411,10 @@ jobs:
|
|||
run: ${{ matrix.dist_command }} --publish never
|
||||
|
||||
- name: Validate packaged bundle (macOS ${{ matrix.arch }})
|
||||
run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin ${{ matrix.arch }}
|
||||
run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Agent Teams AI.app" darwin ${{ matrix.arch }}
|
||||
|
||||
- name: Smoke packaged app (macOS ${{ matrix.arch }})
|
||||
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin
|
||||
run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams AI.app" darwin
|
||||
|
||||
- name: Upload assets to release
|
||||
if: ${{ env.IS_RELEASE_BUILD == 'true' }}
|
||||
|
|
@ -682,6 +682,13 @@ jobs:
|
|||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
declare -A COMPATIBILITY_ALIASES=(
|
||||
["Agent.Teams.AI-arm64.dmg"]="Agent.Teams.AI-${VERSION}-arm64.dmg"
|
||||
["Agent.Teams.AI-x64.dmg"]="Agent.Teams.AI-${VERSION}-x64.dmg"
|
||||
["Agent.Teams.AI.Setup.exe"]="Agent.Teams.AI.Setup.${VERSION}.exe"
|
||||
["Agent.Teams.AI.AppImage"]="Agent.Teams.AI-${VERSION}.AppImage"
|
||||
["agent-teams-ai-amd64.deb"]="agent-teams-ai_${VERSION}_amd64.deb"
|
||||
["agent-teams-ai-x86_64.rpm"]="agent-teams-ai-${VERSION}.x86_64.rpm"
|
||||
["agent-teams-ai.pacman"]="agent-teams-ai-${VERSION}.pacman"
|
||||
["Claude-Agent-Teams-UI-arm64.dmg"]="Agent.Teams.AI-${VERSION}-arm64.dmg"
|
||||
["Claude-Agent-Teams-UI-x64.dmg"]="Agent.Teams.AI-${VERSION}-x64.dmg"
|
||||
["Claude-Agent-Teams-UI-Setup.exe"]="Agent.Teams.AI.Setup.${VERSION}.exe"
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -53,6 +53,7 @@ temp/
|
|||
|
||||
eslint-fix/
|
||||
.eslintcache
|
||||
.eslintcache-fast
|
||||
remotion/*
|
||||
|
||||
.home/
|
||||
|
|
|
|||
16
AGENTS.md
16
AGENTS.md
|
|
@ -22,6 +22,22 @@ Default local run target:
|
|||
- Do not start the browser/web dev mode for normal development or smoke checks. The browser path is limited and lacks the full desktop runtime, IPC, terminal, provider auth, and team lifecycle behavior.
|
||||
- When documenting or recommending startup commands, point contributors to the desktop app unless a task explicitly asks for browser-mode internals.
|
||||
|
||||
Live team smoke runtime:
|
||||
|
||||
- Use the orchestrator source launcher by default for live/dev smoke loops: `/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source`
|
||||
- The source launcher runs `src/entrypoints/cli.tsx` through Bun, so it reflects local orchestrator source edits immediately and cannot accidentally test stale `dist` output.
|
||||
- The source launcher normalizes inherited `NODE_ENV=production` to `NODE_ENV=development`. Release or production-like smoke must use the built wrapper instead of preserving production mode on source.
|
||||
- Local live/prove scripts should use `scripts/lib/live-smoke-runtime.mjs`, which defaults to `cli-source` unless `CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH` is explicitly set.
|
||||
- Source-mode teammate startup can be slower than bundled startup. Live smoke harnesses may raise `CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS` and `CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS` when the test is validating source behavior instead of watchdog latency.
|
||||
- Use the built wrapper only for release or production-like smoke checks. Build first in `/Users/belief/dev/projects/claude/agent_teams_orchestrator` with `bun run build`, then set `CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli`.
|
||||
- Do not use `cli-dev` or `bun run build:dev` as proof for the production wrapper. `cli` reads `dist/local-cli/cli.js`; `cli-dev` reads `dist/local-cli-dev/cli.js`.
|
||||
Fast local lint:
|
||||
|
||||
- Use `pnpm lint:fast:files -- <changed files>` for quick preflight on files you touched.
|
||||
- Use `pnpm lint:fast` for a faster source-tree lint pass when full type-aware lint is too slow.
|
||||
- `lint:fast` intentionally uses `eslint.fast.config.js` without TypeScript project-service rules. It is not a replacement for `pnpm typecheck` or the full `pnpm lint` gate.
|
||||
- Keep using `pnpm typecheck` after TypeScript changes, and use full `pnpm lint` when validating a broad PR or changing lint-sensitive architecture boundaries.
|
||||
|
||||
For new features:
|
||||
|
||||
- Default home for medium and large features: `src/features/<feature-name>/`
|
||||
|
|
|
|||
48
README.md
48
README.md
|
|
@ -23,7 +23,7 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sub>Free desktop app for AI agent teams. Auto-detects Claude/Codex/OpenCode (200+ models). Use the provider access you already have - subscriptions or API keys. Not just coding agents.</sub>
|
||||
<sub>Free desktop app for AI agent teams. Start with a free model with no auth - no signup, API key, or card - or connect Claude/Codex/OpenCode provider access for more models. Not just coding agents.</sub>
|
||||
</p>
|
||||
|
||||
<!--
|
||||
|
|
@ -58,33 +58,33 @@ If you want the FRESHEST version, clone the repo and run it from the `dev` branc
|
|||
<table align="center">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI-arm64.dmg">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Agent.Teams.AI-arm64.dmg">
|
||||
<img src="https://img.shields.io/badge/macOS_Apple_Silicon-.dmg-000000?style=for-the-badge&logo=apple&logoColor=white" alt="macOS Apple Silicon" />
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI-x64.dmg">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Agent.Teams.AI-x64.dmg">
|
||||
<img src="https://img.shields.io/badge/macOS_Intel-.dmg-434343?style=for-the-badge&logo=apple&logoColor=white" alt="macOS Intel" />
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI-Setup.exe">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Agent.Teams.AI.Setup.exe">
|
||||
<img src="https://img.shields.io/badge/Windows-Download_.exe-0078D4?style=for-the-badge&logo=windows&logoColor=white" alt="Windows" />
|
||||
</a>
|
||||
<br />
|
||||
<sub>May trigger SmartScreen — click "More info" → "Run anyway"</sub>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI.AppImage">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Agent.Teams.AI.AppImage">
|
||||
<img src="https://img.shields.io/badge/Linux-Download_.AppImage-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Linux AppImage" />
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI-amd64.deb">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/agent-teams-ai-amd64.deb">
|
||||
<img src="https://img.shields.io/badge/.deb-E95420?style=flat-square&logo=ubuntu&logoColor=white" alt=".deb" />
|
||||
</a>
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI-x86_64.rpm">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/agent-teams-ai-x86_64.rpm">
|
||||
<img src="https://img.shields.io/badge/.rpm-294172?style=flat-square&logo=redhat&logoColor=white" alt=".rpm" />
|
||||
</a>
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI.pacman">
|
||||
<a href="https://github.com/777genius/agent-teams-ai/releases/latest/download/agent-teams-ai.pacman">
|
||||
<img src="https://img.shields.io/badge/.pacman-1793D1?style=flat-square&logo=archlinux&logoColor=white" alt=".pacman" />
|
||||
</a>
|
||||
</td>
|
||||
|
|
@ -96,11 +96,11 @@ If you want the FRESHEST version, clone the repo and run it from the `dev` branc
|
|||
- [Installation](#installation)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [What is this](#what-is-this)
|
||||
- [Developer architecture docs](#developer-architecture-docs)
|
||||
- [Comparison](#comparison)
|
||||
- [Quick start](#quick-start)
|
||||
- [FAQ](#faq)
|
||||
- [Development](#development)
|
||||
- [Developer architecture docs](#developer-architecture-docs)
|
||||
- [Tech stack](#tech-stack)
|
||||
- [Build for distribution](#build-for-distribution)
|
||||
- [Scripts](#scripts)
|
||||
|
|
@ -113,7 +113,7 @@ If you want the FRESHEST version, clone the repo and run it from the `dev` branc
|
|||
|
||||
An orchestration layer for AI agent teams across Claude, Codex, and OpenCode.
|
||||
|
||||
- **Claude + Codex + OpenCode orchestration** — auto-detect available Claude/Codex/OpenCode runtimes and use the provider access you already have - subscriptions or API keys
|
||||
- **Claude + Codex + OpenCode orchestration** — start with a free model with no auth immediately, or auto-detect available Claude/Codex/OpenCode runtimes and use the provider access you already have - subscriptions or API keys
|
||||
- **Assemble your team** — create agent teams with different roles that work autonomously in parallel
|
||||
- **Agents talk to each other** — they communicate, create and manage their own tasks, review, leave comments
|
||||
- **Cross-team communication** — agents can fully communicate across different teams; you can configure or prompt them to collaborate and message each other between teams
|
||||
|
|
@ -141,7 +141,7 @@ An orchestration layer for AI agent teams across Claude, Codex, and OpenCode.
|
|||
|
||||
- **Recent tasks across projects** — browse the latest completed tasks from all your projects in one place
|
||||
|
||||
- **Zero-setup onboarding** — built-in runtime detection and provider authentication
|
||||
- **Zero-setup onboarding** — start with the free model with no auth, then connect paid/account providers only when you need them
|
||||
|
||||
- **Built-in code editor** — edit project files with Git support without leaving the app
|
||||
|
||||
|
|
@ -163,15 +163,6 @@ An orchestration layer for AI agent teams across Claude, Codex, and OpenCode.
|
|||
|
||||
</details>
|
||||
|
||||
## Developer architecture docs
|
||||
|
||||
For feature architecture and implementation guidance:
|
||||
|
||||
- Canonical standard - [docs/FEATURE_ARCHITECTURE_STANDARD.md](docs/FEATURE_ARCHITECTURE_STANDARD.md)
|
||||
- Repo working instructions - [CLAUDE.md](CLAUDE.md)
|
||||
- Feature root guidance - [src/features/README.md](src/features/README.md)
|
||||
- Reference implementation - `src/features/recent-projects`
|
||||
|
||||
## Comparison
|
||||
|
||||
| Feature | Agent Teams | Gastown | Paperclip | Cursor | Claude Code CLI |
|
||||
|
|
@ -201,7 +192,7 @@ For feature architecture and implementation guidance:
|
|||
| **Teammate launch status** | ✅ Know who started, who is stuck, and who replied | ⚠️ Session health, less clear message status | ⚠️ Run status, not live teammate status | ❌ | ⚠️ CLI mailbox, no visual status |
|
||||
| **Org chart / governance** | ⚠️ Roles + approvals, no org chart | ⚠️ Roles + escalation | ✅ Org chart + board governance | ⚠️ Team admin only | ❌ |
|
||||
| **Budget controls** | ⚠️ Cost/token visibility, no hard caps | ⚠️ Cost tiers + digest, no hard caps | ✅ Per-agent budgets + hard stops | ⚠️ Usage + BG spend limits | ⚠️ `/usage` + workspace limits |
|
||||
| **Price** | **Free OSS UI**, provider access needed | Free OSS, runtime plans needed | Free OSS, self-hosted + infra | Free + paid usage | Claude plan or API usage |
|
||||
| **Price** | **Free OSS UI + free model with no auth**, paid providers optional | Free OSS, runtime plans needed | Free OSS, self-hosted + infra | Free + paid usage | Claude plan or API usage |
|
||||
|
||||
Fact sources checked on May 18, 2026: [detailed research notes](docs/research/gastown-paperclip-comparison-2026-05-16.md), [Gastown README](https://github.com/gastownhall/gastown), [Gastown provider guide](https://github.com/gastownhall/gastown/blob/main/docs/agent-provider-integration.md), [Gastown scheduler](https://github.com/gastownhall/gastown/blob/main/docs/design/scheduler.md), [Gastown dashboard source](https://github.com/gastownhall/gastown/blob/main/internal/web/templates/convoy.html), [Gastown release](https://github.com/gastownhall/gastown/releases/tag/v1.1.0), [Paperclip README](https://github.com/paperclipai/paperclip), [Paperclip adapters](https://github.com/paperclipai/paperclip/blob/master/docs/adapters/overview.md), [Paperclip heartbeat protocol](https://github.com/paperclipai/paperclip/blob/master/docs/guides/agent-developer/heartbeat-protocol.md), [Paperclip org chart](https://paperclip.inc/docs/guides/board-operator/org-structure/), [Paperclip OrgChart source](https://github.com/paperclipai/paperclip/blob/master/ui/src/pages/OrgChart.tsx), [Paperclip budgets](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/costs-and-budgets.md), [Paperclip runtime services](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/execution-workspaces-and-runtime-services.md), [Paperclip Kanban source](https://github.com/paperclipai/paperclip/blob/master/ui/src/components/KanbanBoard.tsx), [Paperclip work products](https://github.com/paperclipai/paperclip/blob/master/packages/shared/src/validators/work-product.ts), [Paperclip release](https://github.com/paperclipai/paperclip/releases/tag/v2026.517.0), [Cursor Background Agents](https://docs.cursor.com/en/background-agents), [Cursor Diffs & Review](https://docs.cursor.com/en/agent/review), [Cursor Bugbot](https://docs.cursor.com/en/bugbot), [Cursor pricing](https://docs.cursor.com/en/account/usage), [Claude Code agent teams](https://code.claude.com/docs/en/agent-teams), [Claude Code subagents](https://code.claude.com/docs/en/sub-agents), [Claude Code workflows](https://code.claude.com/docs/en/common-workflows), [Claude Code costs](https://code.claude.com/docs/en/costs), [Claude pricing](https://claude.com/pricing).
|
||||
|
||||
|
|
@ -210,7 +201,7 @@ Fact sources checked on May 18, 2026: [detailed research notes](docs/research/ga
|
|||
## Quick start
|
||||
|
||||
1. **Download** the app for your platform (see [Installation](#installation))
|
||||
2. **Launch the desktop app** - On first run, the setup wizard will detect the runtime and guide provider authentication
|
||||
2. **Launch the desktop app** - start with the free model with no auth, or let the setup wizard detect runtimes and guide provider authentication
|
||||
3. **Create a team** — Pick a project, define roles, write a provisioning prompt
|
||||
4. **Watch** — Agents spawn, create tasks, and work. You see it all on the kanban board
|
||||
|
||||
|
|
@ -224,7 +215,7 @@ Use the desktop app as the primary product. The browser/web path is not needed f
|
|||
<details>
|
||||
<summary><strong>Do I need to install a runtime before using this app?</strong></summary>
|
||||
<br />
|
||||
No. The app guides runtime detection/setup and provider authentication from the UI - just launch and follow the setup wizard.
|
||||
No. You can start with the free model with no auth right away. If you want Claude, Codex, OpenCode/OpenRouter, or other provider-backed models, the app guides runtime detection/setup and provider authentication from the UI.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
@ -242,7 +233,7 @@ Yes. Agents send direct messages, create shared tasks, and leave comments - all
|
|||
<details>
|
||||
<summary><strong>Is it free?</strong></summary>
|
||||
<br />
|
||||
Yes, free and open source. The app has no paid tier of its own. To run agents, you only need access to a supported provider/runtime, such as Anthropic or Codex.
|
||||
Yes. The app is free and open source, and you can start with a free model with no auth - no registration, API keys, or credit card. If you want more models, connect the provider access you already have, such as Claude, Codex, OpenCode/OpenRouter, or other supported runtimes.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
@ -267,6 +258,15 @@ Yes. Run multiple teams in one project or across different projects, even simult
|
|||
|
||||
## Development
|
||||
|
||||
### Developer architecture docs
|
||||
|
||||
For feature architecture and implementation guidance:
|
||||
|
||||
- Canonical standard - [docs/FEATURE_ARCHITECTURE_STANDARD.md](docs/FEATURE_ARCHITECTURE_STANDARD.md)
|
||||
- Repo working instructions - [CLAUDE.md](CLAUDE.md)
|
||||
- Feature root guidance - [src/features/README.md](src/features/README.md)
|
||||
- Reference implementation - `src/features/recent-projects`
|
||||
|
||||
## Tech stack
|
||||
|
||||
Electron 40, React 19, TypeScript 5, Tailwind CSS 3, Zustand 4. Data from `~/.claude/` (session logs, todos, tasks). The desktop app works with local runtime/session state, while some runtime modes may also use provider or startup capability services when required.
|
||||
|
|
|
|||
|
|
@ -212,17 +212,17 @@ Group entries by type: `What's New` > `Improvements` > `Bug Fixes` > `Breaking C
|
|||
|
||||
electron-builder generates these artifacts per platform:
|
||||
|
||||
| Platform | Versioned Name | Stable Name (for /latest/download) |
|
||||
| --------------- | ------------------------------------ | ---------------------------------- |
|
||||
| macOS arm64 DMG | `Agent.Teams.AI-<VER>-arm64.dmg` | `Claude-Agent-Teams-UI-arm64.dmg` |
|
||||
| macOS x64 DMG | `Agent.Teams.AI-<VER>-x64.dmg` | `Claude-Agent-Teams-UI-x64.dmg` |
|
||||
| macOS arm64 ZIP | `Agent.Teams.AI-<VER>-arm64-mac.zip` | - |
|
||||
| macOS x64 ZIP | `Agent.Teams.AI-<VER>-x64-mac.zip` | - |
|
||||
| Windows | `Agent.Teams.AI.Setup.<VER>.exe` | `Claude-Agent-Teams-UI-Setup.exe` |
|
||||
| Linux AppImage | `Agent.Teams.AI-<VER>.AppImage` | `Claude-Agent-Teams-UI.AppImage` |
|
||||
| Linux deb | `agent-teams-ai_<VER>_amd64.deb` | `Claude-Agent-Teams-UI-amd64.deb` |
|
||||
| Linux rpm | `agent-teams-ai-<VER>.x86_64.rpm` | `Claude-Agent-Teams-UI-x86_64.rpm` |
|
||||
| Linux pacman | `agent-teams-ai-<VER>.pacman` | `Claude-Agent-Teams-UI.pacman` |
|
||||
| Platform | Versioned Name | Stable Name (for /latest/download) | Compatibility Alias |
|
||||
| --------------- | ------------------------------------ | ---------------------------------- | ---------------------------------- |
|
||||
| macOS arm64 DMG | `Agent.Teams.AI-<VER>-arm64.dmg` | `Agent.Teams.AI-arm64.dmg` | `Claude-Agent-Teams-UI-arm64.dmg` |
|
||||
| macOS x64 DMG | `Agent.Teams.AI-<VER>-x64.dmg` | `Agent.Teams.AI-x64.dmg` | `Claude-Agent-Teams-UI-x64.dmg` |
|
||||
| macOS arm64 ZIP | `Agent.Teams.AI-<VER>-arm64-mac.zip` | - | - |
|
||||
| macOS x64 ZIP | `Agent.Teams.AI-<VER>-x64-mac.zip` | - | - |
|
||||
| Windows | `Agent.Teams.AI.Setup.<VER>.exe` | `Agent.Teams.AI.Setup.exe` | `Claude-Agent-Teams-UI-Setup.exe` |
|
||||
| Linux AppImage | `Agent.Teams.AI-<VER>.AppImage` | `Agent.Teams.AI.AppImage` | `Claude-Agent-Teams-UI.AppImage` |
|
||||
| Linux deb | `agent-teams-ai_<VER>_amd64.deb` | `agent-teams-ai-amd64.deb` | `Claude-Agent-Teams-UI-amd64.deb` |
|
||||
| Linux rpm | `agent-teams-ai-<VER>.x86_64.rpm` | `agent-teams-ai-x86_64.rpm` | `Claude-Agent-Teams-UI-x86_64.rpm` |
|
||||
| Linux pacman | `agent-teams-ai-<VER>.pacman` | `agent-teams-ai.pacman` | `Claude-Agent-Teams-UI.pacman` |
|
||||
|
||||
## Stable Download Links
|
||||
|
||||
|
|
@ -232,10 +232,11 @@ It starts only after **release-mac** (two matrix jobs), **release-win**, and **r
|
|||
This enables permanent links in README that always point to the latest release:
|
||||
|
||||
```
|
||||
https://github.com/777genius/agent-teams-ai/releases/latest/download/Claude-Agent-Teams-UI-arm64.dmg
|
||||
https://github.com/777genius/agent-teams-ai/releases/latest/download/Agent.Teams.AI-arm64.dmg
|
||||
```
|
||||
|
||||
GitHub automatically redirects `/releases/latest/download/FILENAME` to the asset from the most recent release. No README updates needed when releasing a new version.
|
||||
The `Claude-Agent-Teams-UI-*` aliases are kept only for backward compatibility with older links and clients.
|
||||
|
||||
## macOS Code Signing
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,29 @@ Desktop launches use the app-managed process backend by default. That is the sup
|
|||
normal app launches because the app owns the process lifecycle, runtime logs, cleanup, and bootstrap
|
||||
evidence.
|
||||
|
||||
## Live Smoke Runtime Launcher
|
||||
|
||||
Live/dev smoke tests should use the orchestrator source launcher by default:
|
||||
|
||||
```bash
|
||||
CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source \
|
||||
pnpm vitest run --maxWorkers 1 --minWorkers 1 test/main/services/team/AnthropicLaunchSelection.live.test.ts
|
||||
```
|
||||
|
||||
`cli-source` runs `src/entrypoints/cli.tsx` directly through Bun. Use it while developing launch/runtime code so the smoke test cannot accidentally pass or fail against a stale `dist/local-cli/cli.js` bundle.
|
||||
|
||||
For release or production-like smoke checks, test the built wrapper explicitly:
|
||||
|
||||
```bash
|
||||
cd /Users/belief/dev/projects/claude/agent_teams_orchestrator
|
||||
bun run build
|
||||
cd /Users/belief/dev/projects/claude/claude_team
|
||||
CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli \
|
||||
pnpm vitest run --maxWorkers 1 --minWorkers 1 test/main/services/team/AnthropicLaunchSelection.live.test.ts
|
||||
```
|
||||
|
||||
The built wrapper `cli` reads `dist/local-cli/cli.js`. `cli-dev` reads `dist/local-cli-dev/cli.js`; it is useful for dev-bundle checks, but it is not the production wrapper.
|
||||
|
||||
For local debugging, force pane-backed teammates through `tmux`:
|
||||
|
||||
```bash
|
||||
|
|
@ -81,6 +104,40 @@ Use this mode to inspect interactive CLI behavior, terminal prompts, and pane ou
|
|||
as equivalent to the process backend for recovery semantics; persisted pane IDs can help discovery,
|
||||
but app restart does not make old panes a fully app-owned runtime again.
|
||||
|
||||
## Live Smoke Runtime Launcher
|
||||
|
||||
Live/dev smoke checks should run the orchestrator from source unless the test explicitly says it is
|
||||
validating packaged output. This keeps app smoke tests aligned with the source tree and avoids a stale
|
||||
`dist` bundle hiding runtime changes.
|
||||
|
||||
Default live/dev smoke launcher:
|
||||
|
||||
```bash
|
||||
export CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source
|
||||
```
|
||||
|
||||
The source launcher executes `src/entrypoints/cli.tsx` through Bun. It is the right default for local
|
||||
debug loops, live model/provider checks, and cross-repo runtime fixes.
|
||||
It normalizes inherited `NODE_ENV=production` to `NODE_ENV=development`, because source smoke is a
|
||||
dev/runtime validation path. If you need production semantics, run the release smoke path below.
|
||||
Local live/prove scripts should resolve their default CLI through `scripts/lib/live-smoke-runtime.mjs`,
|
||||
which points at `cli-source` unless `CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH` is explicitly set.
|
||||
Source-mode teammate startup can be slower than bundled startup, so live smoke harnesses may set
|
||||
`CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS` and
|
||||
`CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS` to larger values when they are validating source
|
||||
behavior instead of watchdog latency.
|
||||
|
||||
Release or production-like smoke checks must validate the built wrapper:
|
||||
|
||||
```bash
|
||||
cd /Users/belief/dev/projects/claude/agent_teams_orchestrator
|
||||
bun run build
|
||||
export CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli
|
||||
```
|
||||
|
||||
`cli` reads `dist/local-cli/cli.js`. `cli-dev` reads `dist/local-cli-dev/cli.js`, so a passing
|
||||
`cli-dev` smoke is not proof that the production wrapper is fresh.
|
||||
|
||||
## Member State Meanings
|
||||
|
||||
Common `launch-state.json` cases:
|
||||
|
|
|
|||
35
docs/team-management/member-mcp-bootstrap-spike.md
Normal file
35
docs/team-management/member-mcp-bootstrap-spike.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Per-Member MCP Bootstrap Spike
|
||||
|
||||
Date: 2026-05-19
|
||||
|
||||
## Question
|
||||
|
||||
Can the app pass per-member MCP inheritance or allowlist settings through Claude Code native agent-team bootstrap?
|
||||
|
||||
## Findings
|
||||
|
||||
- The app currently starts native Claude-led teams with one lead CLI process and one generated `--mcp-config`. That generated config only contains the app-owned `agent-teams` server. User, project, and local MCP servers are inherited through `--setting-sources user,project,local`.
|
||||
- Claude Code docs for agent teams state that teammates load MCP servers from project and user settings like regular sessions. They also state that subagent-definition `mcpServers` frontmatter is not applied when the definition runs as an agent-team teammate: https://code.claude.com/docs/en/agent-teams
|
||||
- Claude Code docs for subagents support `mcpServers` for normal subagents and main sessions, but agent teams explicitly exclude that field on the teammate path: https://code.claude.com/docs/en/sub-agents
|
||||
- Local Claude Code `2.1.119` supports `--mcp-config`, `--setting-sources`, and `--strict-mcp-config`. The public CLI reference documents those flags: https://code.claude.com/docs/en/cli-usage
|
||||
- A local probe with `claude --bare --team-bootstrap-spec <spec>` on `2.1.119` exits with `error: unknown option '--team-bootstrap-spec'`, so the hidden app bootstrap path cannot be validated as a public CLI contract from the installed binary.
|
||||
|
||||
## Decision
|
||||
|
||||
Per-member MCP settings should be treated as a gated app feature until the native bootstrap contract is proven to apply them on initial spawn.
|
||||
|
||||
The safe implementation path is:
|
||||
|
||||
1. Persist `mcpPolicy` on members.
|
||||
2. Surface the policy in the roster editor.
|
||||
3. Apply the policy only on app-controlled teammate launches/restarts where the app owns the CLI args.
|
||||
4. Keep initial native bootstrap behavior unchanged until the app either moves that path to app-managed teammate launch or detects a Claude Code capability that supports per-member MCP in bootstrap specs.
|
||||
|
||||
## Current Runtime Semantics
|
||||
|
||||
- `inheritLead`: keep existing behavior.
|
||||
- `inheritScopes`: app-controlled teammate launches can narrow `--setting-sources`.
|
||||
- `strictAllowlist`: app-controlled teammate launches generate a strict MCP config containing `agent-teams` plus selected server definitions.
|
||||
- `appOnly`: app-controlled teammate launches generate a strict MCP config containing only `agent-teams`.
|
||||
|
||||
`agent-teams` must remain non-removable because it carries team messaging and task tooling.
|
||||
256
eslint.fast.config.js
Normal file
256
eslint.fast.config.js
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import boundaries from 'eslint-plugin-boundaries';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import security from 'eslint-plugin-security';
|
||||
import sonarjs from 'eslint-plugin-sonarjs';
|
||||
import tailwindcss from 'eslint-plugin-tailwindcss';
|
||||
import globals from 'globals';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: 'fast-linter-options',
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'off',
|
||||
},
|
||||
},
|
||||
|
||||
globalIgnores([
|
||||
'dist/**',
|
||||
'dist-electron/**',
|
||||
'build/**',
|
||||
'node_modules/**',
|
||||
'out/**',
|
||||
'landing/.nuxt/**',
|
||||
]),
|
||||
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
{
|
||||
name: 'fast-known-plugin-namespaces',
|
||||
plugins: {
|
||||
boundaries,
|
||||
import: importPlugin,
|
||||
security,
|
||||
sonarjs,
|
||||
tailwindcss,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'no-unsafe-finally': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-typescript',
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: false,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-imports',
|
||||
files: ['src/**/*.{js,jsx,ts,tsx}', 'test/**/*.{ts,tsx}', 'packages/agent-graph/src/**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
'simple-import-sort': simpleImportSort,
|
||||
},
|
||||
rules: {
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['^\\u0000'],
|
||||
['^node:'],
|
||||
['^react', '^react-dom'],
|
||||
['^@?\\w'],
|
||||
['^@/'],
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
['^.+\\u0000$'],
|
||||
],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-node-globals',
|
||||
files: ['src/main/**/*.ts', 'src/preload/**/*.ts', 'scripts/**/*.{js,mjs,ts}', 'test/**/*.ts'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-browser-globals',
|
||||
files: ['src/renderer/**/*.{ts,tsx}', 'src/features/**/renderer/**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-react',
|
||||
files: ['src/renderer/**/*.{tsx,ts}', 'src/features/**/renderer/**/*.{tsx,ts}'],
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
'jsx-a11y': jsxA11y,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...reactPlugin.configs.recommended.rules,
|
||||
...reactPlugin.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
...jsxA11y.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'react-hooks/rules-of-hooks': 'warn',
|
||||
'react-hooks/globals': 'off',
|
||||
'react-hooks/purity': 'off',
|
||||
'react-hooks/refs': 'off',
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/preserve-manual-memoization': 'off',
|
||||
'react-hooks/immutability': 'off',
|
||||
'jsx-a11y/click-events-have-key-events': 'warn',
|
||||
'jsx-a11y/no-static-element-interactions': 'warn',
|
||||
'jsx-a11y/label-has-associated-control': 'warn',
|
||||
'jsx-a11y/no-noninteractive-tabindex': 'warn',
|
||||
'jsx-a11y/no-autofocus': 'off',
|
||||
'react/function-component-definition': [
|
||||
'warn',
|
||||
{
|
||||
namedComponents: 'arrow-function',
|
||||
unnamedComponents: 'arrow-function',
|
||||
},
|
||||
],
|
||||
'react/jsx-key': [
|
||||
'error',
|
||||
{
|
||||
checkFragmentShorthand: true,
|
||||
checkKeyMustBeforeSpread: true,
|
||||
},
|
||||
],
|
||||
'react/self-closing-comp': ['error', { component: true, html: true }],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-feature-entrypoints',
|
||||
files: [
|
||||
'src/main/**/*.{ts,tsx}',
|
||||
'src/preload/**/*.{ts,tsx}',
|
||||
'src/renderer/**/*.{ts,tsx}',
|
||||
'src/shared/**/*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
'@features/*/contracts/*',
|
||||
'@features/*/core/**',
|
||||
'@features/*/main/*',
|
||||
'@features/*/preload/*',
|
||||
'@features/*/renderer/*',
|
||||
],
|
||||
message: 'Import feature public entrypoints only.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-feature-core-guards',
|
||||
files: ['src/features/*/core/{domain,application}/**/*.ts'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{ name: 'electron', message: 'Feature core must stay Electron-free.' },
|
||||
{ name: 'fastify', message: 'Feature core must stay transport-free.' },
|
||||
{ name: 'child_process', message: 'Feature core must not spawn processes directly.' },
|
||||
{
|
||||
name: 'node:child_process',
|
||||
message: 'Feature core must not spawn processes directly.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ['@main/*', '@preload/*', '@renderer/*'],
|
||||
message: 'Feature core must stay process-agnostic.',
|
||||
},
|
||||
{
|
||||
group: ['@features/*/main/**', '@features/*/preload/**', '@features/*/renderer/**'],
|
||||
message: 'Feature core must not import runtime or transport layers.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fast-feature-renderer-ui-guards',
|
||||
files: ['src/features/*/renderer/ui/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{ name: '@renderer/api', message: 'renderer/ui must stay presentational.' },
|
||||
{ name: '@renderer/store', message: 'renderer/ui must stay store-free.' },
|
||||
{ name: 'electron', message: 'renderer/ui must stay Electron-free.' },
|
||||
],
|
||||
patterns: [
|
||||
{ group: ['@main/*'], message: 'renderer/ui must not import main modules.' },
|
||||
{ group: ['@renderer/store/*'], message: 'renderer/ui must stay store-free.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
13
landing/AGENTS.md
Normal file
13
landing/AGENTS.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Landing Visual QA
|
||||
|
||||
- For cyberpunk landing work, verify layout with browser/runtime measurements, not by eye only.
|
||||
- Use Chrome DevTools MCP for landing visual QA. Do not use Brave real browser as a fallback for this landing unless the user explicitly asks for Brave.
|
||||
- First try the direct `mcp__chrome_devtools__*` namespace for viewport screenshots, DOM rectangles, computed styles, console errors, network state, and performance checks.
|
||||
- If the direct `mcp__chrome_devtools__*` namespace is exposed but the transport is closed, diagnose/fix that first. The stable global config should point to a fixed local install, not `npx @latest`: `/usr/local/bin/node /Users/belief/.codex/mcp/chrome-devtools/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js --isolated --viewport=1440x900 --logFile /Users/belief/.codex/log/chrome-devtools-mcp.log --no-usage-statistics --no-performance-crux`.
|
||||
- If the current Codex thread cannot recover the direct transport after the config is fixed, use only the official Chrome DevTools MCP isolated stdio client as the temporary fallback. The current server uses newline-delimited JSON-RPC over stdio. Kill the spawned MCP process after screenshots/metrics finish.
|
||||
- If `chrome-devtools` is enabled in `~/.codex/config.toml` but no `mcp__chrome_devtools__*` tools are exposed in the current thread, treat it as a Codex Desktop tool-schema/session exposure issue. Start a fresh thread/session when possible; otherwise use the official isolated Chrome DevTools MCP stdio client above, not Brave.
|
||||
- Required visual viewports for this landing: `2048x1152`, `1680x941`, `1366x768`, and `390x844`.
|
||||
- For the HUD header, measure the brand panel, nav rail, action panel, nav item centers, and action button centers with `getBoundingClientRect()`. Check that hover/focus glow is clipped to the angular panel shape.
|
||||
- For the hero video scene, measure robot foot baselines against the video frame top/bottom/side edges. Top-row robots should stand on the top edge; bottom-row robots should stand on the bottom edge; side robots should not cover the video content randomly.
|
||||
- Keep the header as live DOM plus SVG. Do not ship a PNG header asset except for reference images stored under `assets/images/references/`.
|
||||
- Do not run `pnpm generate` while the landing dev server is running. Stop dev first, generate, then run cleanup before starting dev again.
|
||||
|
|
@ -24,17 +24,18 @@
|
|||
radial-gradient(circle at 86% 70%, rgba(255, 43, 255, 0.14), transparent 34%),
|
||||
linear-gradient(180deg, var(--cyber-bg-0), var(--cyber-bg-1) 58%, var(--cyber-bg-0));
|
||||
--cyber-monterey-bg:
|
||||
radial-gradient(circle at 76% 22%, rgba(138, 47, 255, 0.76), transparent 30%),
|
||||
radial-gradient(circle at 18% 76%, rgba(37, 8, 128, 0.72), transparent 36%),
|
||||
linear-gradient(180deg, #180061 0%, #3200a2 46%, #130042 100%);
|
||||
radial-gradient(circle at 76% 22%, rgba(47, 125, 255, 0.22), transparent 30%),
|
||||
radial-gradient(circle at 18% 76%, rgba(37, 8, 128, 0.22), transparent 36%),
|
||||
radial-gradient(circle at 88% 68%, rgba(255, 43, 255, 0.1), transparent 34%),
|
||||
linear-gradient(180deg, #030614 0%, #07122a 46%, #02050d 100%);
|
||||
--cyber-monterey-before-bg:
|
||||
radial-gradient(circle at 18% 34%, rgba(2, 5, 13, 0.62), rgba(2, 5, 13, 0.14) 34%, transparent 62%),
|
||||
linear-gradient(90deg, rgba(2, 5, 13, 0.48) 0%, rgba(2, 5, 13, 0.17) 42%, rgba(2, 5, 13, 0.04) 64%, rgba(2, 5, 13, 0.3) 100%);
|
||||
--cyber-monterey-after-bg:
|
||||
linear-gradient(180deg, rgba(2, 5, 13, 0.92) 0%, rgba(2, 5, 13, 0.62) 15%, rgba(2, 5, 13, 0.08) 44%, rgba(2, 5, 13, 0.68) 100%),
|
||||
radial-gradient(circle at 64% 42%, transparent 0 26%, rgba(2, 5, 13, 0.24) 70%, rgba(2, 5, 13, 0.54) 100%);
|
||||
--cyber-monterey-canvas-opacity: 1;
|
||||
--cyber-monterey-canvas-filter: blur(4px) saturate(1.22) brightness(1.08) contrast(1.08);
|
||||
--cyber-monterey-canvas-opacity: 0.42;
|
||||
--cyber-monterey-canvas-filter: blur(4px) saturate(0.78) brightness(0.62) contrast(1.08);
|
||||
--cyber-monterey-canvas-blend: normal;
|
||||
--cyber-background-bg:
|
||||
radial-gradient(circle at 72% 28%, rgba(0, 234, 255, 0.1), transparent 30%),
|
||||
|
|
@ -255,6 +256,10 @@
|
|||
background: var(--cyber-hero-bg);
|
||||
}
|
||||
|
||||
#hero.cyber-hero {
|
||||
padding-top: 120px;
|
||||
}
|
||||
|
||||
.cyber-hero__background,
|
||||
.cyber-hero__monterey,
|
||||
.cyber-hero__wash,
|
||||
|
|
@ -454,7 +459,7 @@
|
|||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.18em;
|
||||
font-size: clamp(2.55rem, 5.1vw, 5.7rem);
|
||||
font-size: clamp(2.45rem, 4.8vw, 5.35rem);
|
||||
line-height: 1;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
|
|
@ -1468,7 +1473,7 @@
|
|||
}
|
||||
|
||||
.cyber-hero__title {
|
||||
font-size: clamp(3rem, 4.6vw, 4.8rem);
|
||||
font-size: clamp(2.8rem, 4.2vw, 4.45rem);
|
||||
}
|
||||
|
||||
.cyber-scene {
|
||||
|
|
@ -1609,7 +1614,8 @@
|
|||
}
|
||||
|
||||
.cyber-hero__title {
|
||||
font-size: clamp(2.55rem, 13vw, 4rem);
|
||||
gap: 0.12em;
|
||||
font-size: clamp(2rem, 9.4vw, 3.1rem);
|
||||
}
|
||||
|
||||
.cyber-hero__slogan {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ const { baseURL } = useRuntimeConfig().app;
|
|||
<NuxtLink to="/" class="app-logo" :prefetch="false">
|
||||
<img
|
||||
:src="`${baseURL}logo-192.png`"
|
||||
alt="Agent Teams"
|
||||
alt="Agent Teams AI"
|
||||
class="app-logo__img"
|
||||
width="36"
|
||||
height="36"
|
||||
>
|
||||
<span class="app-logo__text">Agent Teams</span>
|
||||
<span class="app-logo__text">Agent Teams AI</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
|
|
@ -46,12 +46,13 @@ const { baseURL } = useRuntimeConfig().app;
|
|||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 16px rgba(0, 240, 255, 0.22);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.v-theme--light .app-logo__text {
|
||||
background: linear-gradient(135deg, #1e293b, #0891b2);
|
||||
background: linear-gradient(135deg, #ffffff 0%, #dff8ff 44%, #43efff 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
|
|
|
|||
|
|
@ -43,16 +43,25 @@ const docsHref = computed(() => {
|
|||
|
||||
<style scoped>
|
||||
.app-footer {
|
||||
--footer-bg:
|
||||
linear-gradient(180deg, rgba(3, 10, 22, 0.96) 0%, rgba(2, 6, 16, 0.98) 100%);
|
||||
--footer-wall-border: rgba(0, 234, 255, 0.28);
|
||||
--footer-wall-highlight: rgba(255, 255, 255, 0.06);
|
||||
|
||||
position: relative;
|
||||
border-top: 1px solid var(--at-c-border);
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid var(--footer-wall-border);
|
||||
padding: 28px 0 22px;
|
||||
isolation: isolate;
|
||||
background: var(--footer-bg);
|
||||
box-shadow:
|
||||
0 -28px 70px rgba(0, 0, 0, 0.34),
|
||||
0 -1px 0 var(--footer-wall-highlight) inset;
|
||||
}
|
||||
|
||||
.app-footer__robot-stage {
|
||||
position: absolute;
|
||||
right: clamp(24px, 7vw, 112px);
|
||||
bottom: calc(100% - 5px);
|
||||
bottom: calc(100% - 11px);
|
||||
z-index: 2;
|
||||
width: clamp(178px, 16vw, 236px);
|
||||
pointer-events: none;
|
||||
|
|
@ -92,7 +101,7 @@ const docsHref = computed(() => {
|
|||
|
||||
.app-footer__copy {
|
||||
font-size: 13px;
|
||||
opacity: 0.5;
|
||||
color: rgba(244, 247, 255, 0.72);
|
||||
font-family: var(--at-font-mono);
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +115,7 @@ const docsHref = computed(() => {
|
|||
color: var(--at-c-cyan);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
font-family: var(--at-font-mono);
|
||||
}
|
||||
|
|
@ -122,11 +131,19 @@ const docsHref = computed(() => {
|
|||
}
|
||||
|
||||
.v-theme--light .app-footer {
|
||||
border-top-color: var(--at-c-border);
|
||||
--footer-bg:
|
||||
linear-gradient(180deg, rgba(230, 240, 247, 0.98) 0%, rgba(218, 229, 238, 0.98) 100%);
|
||||
--footer-wall-border: rgba(8, 88, 112, 0.24);
|
||||
--footer-wall-highlight: rgba(255, 255, 255, 0.82);
|
||||
|
||||
border-top-color: var(--footer-wall-border);
|
||||
box-shadow:
|
||||
0 -32px 74px rgba(62, 84, 104, 0.2),
|
||||
0 -1px 0 rgba(255, 255, 255, 0.92) inset;
|
||||
}
|
||||
|
||||
.v-theme--light .app-footer__copy {
|
||||
opacity: 0.72;
|
||||
color: rgba(42, 50, 61, 0.74);
|
||||
}
|
||||
|
||||
.v-theme--light .app-footer__link {
|
||||
|
|
|
|||
|
|
@ -183,12 +183,12 @@ const navItems = computed(() => [
|
|||
--header-cyan: var(--cyber-cyan);
|
||||
--header-violet: var(--cyber-violet);
|
||||
--header-magenta: var(--cyber-magenta);
|
||||
--header-height: 128px;
|
||||
--header-height: 126px;
|
||||
--header-panel-height: 86px;
|
||||
--header-action-size: clamp(54px, 3.25vw, 66px);
|
||||
--header-github-width: clamp(150px, 9.7vw, 204px);
|
||||
--header-brand-icon: clamp(58px, 4.1vw, 76px);
|
||||
--header-brand-text: clamp(24px, 1.55vw, 34px);
|
||||
--header-brand-icon: clamp(52px, 3.7vw, 68px);
|
||||
--header-brand-text: clamp(23px, 1.42vw, 32px);
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -299,7 +299,7 @@ const navItems = computed(() => [
|
|||
width: 100%;
|
||||
height: var(--header-panel-height);
|
||||
min-width: 0;
|
||||
padding: 0 64px 0 clamp(38px, 4.2vw, 82px);
|
||||
padding: 0 46px 0 clamp(24px, 2vw, 38px);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
|
|
@ -548,12 +548,12 @@ const navItems = computed(() => [
|
|||
|
||||
@media (max-width: 1439px) {
|
||||
.app-header {
|
||||
--header-height: 104px;
|
||||
--header-height: 112px;
|
||||
--header-panel-height: 72px;
|
||||
--header-action-size: 54px;
|
||||
--header-github-width: 124px;
|
||||
--header-brand-icon: 48px;
|
||||
--header-brand-text: 16px;
|
||||
--header-brand-icon: 44px;
|
||||
--header-brand-text: 15px;
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
@ -563,8 +563,8 @@ const navItems = computed(() => [
|
|||
|
||||
.app-header__brand-frame {
|
||||
height: 72px;
|
||||
padding-left: 38px;
|
||||
padding-right: 38px;
|
||||
padding-left: 24px;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.app-header__brand-frame :deep(.app-logo__text) {
|
||||
|
|
@ -598,7 +598,7 @@ const navItems = computed(() => [
|
|||
@media (max-width: 1120px) {
|
||||
.app-header {
|
||||
--header-github-width: 104px;
|
||||
--header-brand-text: 14px;
|
||||
--header-brand-text: 13px;
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
@ -606,8 +606,8 @@ const navItems = computed(() => [
|
|||
}
|
||||
|
||||
.app-header__brand-frame {
|
||||
padding-left: 26px;
|
||||
padding-right: 34px;
|
||||
padding-left: 18px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.app-header__nav {
|
||||
|
|
@ -628,11 +628,11 @@ const navItems = computed(() => [
|
|||
|
||||
@media (max-width: 1279px) and (min-width: 768px) {
|
||||
.app-header {
|
||||
--header-height: 88px;
|
||||
--header-height: 104px;
|
||||
--header-panel-height: 64px;
|
||||
--header-action-size: clamp(40px, 5vw, 48px);
|
||||
--header-brand-icon: clamp(38px, 4.8vw, 44px);
|
||||
--header-brand-text: clamp(11px, 1.35vw, 14px);
|
||||
--header-brand-icon: clamp(34px, 4.6vw, 42px);
|
||||
--header-brand-text: clamp(10px, 1.2vw, 12px);
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
@ -642,8 +642,8 @@ const navItems = computed(() => [
|
|||
|
||||
.app-header__brand-frame {
|
||||
height: 64px;
|
||||
padding-left: clamp(18px, 2.4vw, 30px);
|
||||
padding-right: clamp(22px, 3vw, 34px);
|
||||
padding-left: clamp(12px, 1.7vw, 18px);
|
||||
padding-right: clamp(16px, 2vw, 22px);
|
||||
}
|
||||
|
||||
.app-header__brand-frame :deep(.app-logo) {
|
||||
|
|
@ -658,7 +658,7 @@ const navItems = computed(() => [
|
|||
--nav-pad-start: clamp(14px, 3.4cqw, 28px);
|
||||
--nav-pad-end: clamp(18px, 4cqw, 34px);
|
||||
|
||||
top: 12px;
|
||||
top: calc((var(--header-height) - var(--header-panel-height)) / 2);
|
||||
left: calc(456 / 2048 * 100%);
|
||||
right: calc((2048 - 1568) / 2048 * 100%);
|
||||
height: 64px;
|
||||
|
|
@ -715,6 +715,8 @@ const navItems = computed(() => [
|
|||
@media (max-width: 767px) {
|
||||
.app-header {
|
||||
height: 64px;
|
||||
--header-brand-icon: 34px;
|
||||
--header-brand-text: 12px;
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ const rows = computed<ComparisonRow[]>(() => [
|
|||
},
|
||||
{
|
||||
feature: t('comparison.features.price'),
|
||||
us: { status: 'free', note: 'OSS, provider access needed' },
|
||||
us: { status: 'free', note: 'OSS + free model with no auth, paid providers optional' },
|
||||
gastown: { status: 'free', note: 'OSS, runtime plans needed' },
|
||||
paperclip: { status: 'free', note: 'OSS, self-hosted + infra' },
|
||||
cursor: { status: 'text', note: 'Free + paid usage' },
|
||||
|
|
@ -511,7 +511,7 @@ function getStatusIcon(status: string): string {
|
|||
.comparison-table__robot {
|
||||
position: absolute;
|
||||
right: clamp(28px, 7vw, 96px);
|
||||
bottom: calc(100% - 5px);
|
||||
bottom: calc(100% - 20px);
|
||||
z-index: 4;
|
||||
width: clamp(82px, 7.2vw, 124px);
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -892,6 +892,11 @@ const releaseDate = computed(() => {
|
|||
color: #64748b;
|
||||
}
|
||||
|
||||
.v-theme--light .download-section__btn {
|
||||
color: #f8fbff;
|
||||
text-shadow: 0 1px 8px rgba(15, 23, 42, 0.34);
|
||||
}
|
||||
|
||||
.v-theme--light .download-section__release-info {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,9 +131,10 @@ onUnmounted(() => {
|
|||
<v-container class="cyber-hero__container">
|
||||
<div class="cyber-hero__layout">
|
||||
<div class="cyber-hero__copy">
|
||||
<h1 class="cyber-hero__title" aria-label="Agent Teams">
|
||||
<h1 class="cyber-hero__title" aria-label="Agent Teams AI">
|
||||
<span>Agent</span>
|
||||
<span class="cyber-hero__title-accent">Teams</span>
|
||||
<span class="cyber-hero__title-accent">AI</span>
|
||||
</h1>
|
||||
|
||||
<p class="cyber-hero__slogan cyber-panel">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { mdiOpenSourceInitiative } from '@mdi/js'
|
||||
import { mdiCheckCircleOutline, mdiOpenSourceInitiative } from '@mdi/js'
|
||||
import { useLandingContent } from '~/composables/useLandingContent'
|
||||
|
||||
const { content } = useLandingContent()
|
||||
|
|
@ -43,6 +43,23 @@ function onGetStarted() {
|
|||
<span v-if="plan.period" class="pricing-card__period">/ {{ plan.period }}</span>
|
||||
</div>
|
||||
<p class="pricing-card__description">{{ plan.description }}</p>
|
||||
<div class="pricing-card__callout">
|
||||
{{ t('pricing.freeModelCallout') }}
|
||||
</div>
|
||||
<ul v-if="plan.features.length" class="pricing-card__features">
|
||||
<li
|
||||
v-for="feature in plan.features"
|
||||
:key="feature"
|
||||
class="pricing-card__feature"
|
||||
>
|
||||
<v-icon
|
||||
size="16"
|
||||
class="pricing-card__feature-icon"
|
||||
:icon="mdiCheckCircleOutline"
|
||||
/>
|
||||
<span>{{ feature }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -177,6 +194,41 @@ function onGetStarted() {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.pricing-card__callout {
|
||||
margin-top: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(57, 255, 20, 0.22);
|
||||
border-radius: 10px;
|
||||
background: rgba(57, 255, 20, 0.08);
|
||||
color: #d9ffe0;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pricing-card__features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 18px 0 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pricing-card__feature {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
color: #aab4d4;
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pricing-card__feature-icon {
|
||||
color: #39ff14;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.pricing-card__btn--primary {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
|
|
@ -243,6 +295,20 @@ function onGetStarted() {
|
|||
color: #475569;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-card__callout {
|
||||
border-color: rgba(18, 161, 80, 0.22);
|
||||
background: rgba(18, 161, 80, 0.08);
|
||||
color: #116b3b;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-card__feature {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-card__feature-icon {
|
||||
color: #12a150;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-section__note {
|
||||
color: #56617c;
|
||||
opacity: 1;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "هل هو مجاني فعلاً؟",
|
||||
"answer": "نعم. التطبيق مجاني ومفتوح المصدر. التطبيق نفسه لا يملك خطة مدفوعة. لتشغيل الوكلاء تحتاج فقط إلى وصول إلى مزود أو runtime مدعوم مثل Anthropic أو Codex."
|
||||
"answer": "نعم. التطبيق مجاني ومفتوح المصدر، ويمكنك البدء بنموذج مجاني بدون مصادقة - بدون تسجيل أو مفتاح API أو بطاقة ائتمان. إذا أردت نماذج أكثر، وصّل Claude أو Codex أو OpenCode/OpenRouter أو مزوداً مدعوماً آخر."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "ماذا أحتاج للبدء؟",
|
||||
"answer": "فقط ثبّت التطبيق - وسيرشدك من الواجهة لاكتشاف الـ runtime وتسجيل دخول المزود. البدء بدون إعداد يجعلك تعمل في دقائق."
|
||||
"answer": "فقط ثبّت التطبيق - ابدأ بالنموذج المجاني بدون مصادقة، ثم وصّل نماذج provider-backed من الواجهة فقط عندما تحتاجها."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "مجاني",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "كل شيء متضمن. بدون حدود، بدون مفاتيح API، بدون بطاقة ائتمان.",
|
||||
"description": "ابدأ بالنموذج المجاني بدون مصادقة المضمّن. بدون تسجيل، بدون مفتاح API، بدون بطاقة.",
|
||||
"features": [
|
||||
"نموذج مجاني بدون مصادقة لأول تشغيل",
|
||||
"لا تحتاج حساباً أو تسجيل دخول مزود للتجربة",
|
||||
"وصول اختياري إلى Claude وCodex وOpenCode",
|
||||
"فرق وكلاء غير محدودة",
|
||||
"لوحة كانبان بتحديثات فورية",
|
||||
"مراجعة كود مع عرض diff",
|
||||
"تواصل بين الفرق",
|
||||
"وضع فردي وجماعي",
|
||||
"مراقبة عمليات مباشرة",
|
||||
"محرر كود مدمج",
|
||||
"تكامل MCP"
|
||||
"وضع فردي وجماعي"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "Ist es wirklich kostenlos?",
|
||||
"answer": "Ja. Die App ist kostenlos und Open Source. Die App selbst hat kein Bezahlmodell. Um Agenten auszuführen, brauchen Sie nur Zugriff auf einen unterstützten Provider bzw. Runtime wie Anthropic oder Codex."
|
||||
"answer": "Ja. Die App ist kostenlos und Open Source, und Sie können mit einem kostenlosen Modell ohne Authentifizierung starten - ohne Registrierung, API-Schlüssel oder Kreditkarte. Für mehr Modelle verbinden Sie Claude, Codex, OpenCode/OpenRouter oder einen anderen unterstützten Provider."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "Was brauche ich zum Start?",
|
||||
"answer": "Einfach die App installieren - sie führt Sie in der UI durch Runtime-Erkennung und Provider-Authentifizierung. Zero-Setup-Onboarding bringt Sie in Minuten zum Laufen."
|
||||
"answer": "Einfach die App installieren - starten Sie mit dem kostenlosen Modell ohne Authentifizierung und verbinden Sie provider-backed Modelle in der UI erst, wenn Sie sie brauchen."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Kostenlos",
|
||||
"price": "0€",
|
||||
"period": "",
|
||||
"description": "Alles inklusive. Keine Limits, keine API-Schlüssel, keine Kreditkarte.",
|
||||
"description": "Starten Sie mit dem enthaltenen kostenlosen Modell ohne Authentifizierung. Keine Registrierung, kein API-Schlüssel, keine Kreditkarte.",
|
||||
"features": [
|
||||
"Kostenloses Modell ohne Authentifizierung für erste Läufe",
|
||||
"Kein Konto und kein Provider-Login zum Testen erforderlich",
|
||||
"Optionaler Zugriff auf Claude, Codex und OpenCode",
|
||||
"Unbegrenzte Agenten-Teams",
|
||||
"Kanban-Board mit Echtzeit-Updates",
|
||||
"Code-Review mit Diff-Ansicht",
|
||||
"Teamübergreifende Kommunikation",
|
||||
"Solo- und Team-Modi",
|
||||
"Live-Prozessüberwachung",
|
||||
"Integrierter Code-Editor",
|
||||
"MCP-Integration"
|
||||
"Solo- und Team-Modi"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "Is it really free?",
|
||||
"answer": "Yes. The app itself is free and open source. The app has no paid tier of its own. To run agents, you only need access to a supported provider/runtime, such as Anthropic or Codex."
|
||||
"answer": "Yes. The app is free and open source, and you can start with a free model with no auth - no registration, API keys, or credit card. If you want more models, connect Claude, Codex, OpenCode/OpenRouter, or another supported provider."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "What do I need to get started?",
|
||||
"answer": "Just install the app - it guides runtime detection and provider authentication from the UI. Zero-setup onboarding gets you running in minutes."
|
||||
"answer": "Just install the app - start with the free model with no auth, then connect provider-backed models from the UI only when you need them."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Free",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Everything included. No limits, no API keys, no credit card.",
|
||||
"description": "Start with the included free model with no auth. No signup, no API key, no credit card.",
|
||||
"features": [
|
||||
"Free model with no auth for first runs",
|
||||
"No account or provider login required to try",
|
||||
"Optional Claude, Codex, and OpenCode provider access",
|
||||
"Unlimited agent teams",
|
||||
"Kanban board with real-time updates",
|
||||
"Code review with diff view",
|
||||
"Cross-team communication",
|
||||
"Solo & team modes",
|
||||
"Live process monitoring",
|
||||
"Built-in code editor",
|
||||
"MCP integration"
|
||||
"Solo & team modes"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "¿Es realmente gratis?",
|
||||
"answer": "Sí. La app es gratuita y de código abierto. La app no tiene un plan de pago propio. Para ejecutar agentes solo necesitas acceso a un proveedor/runtime compatible, como Anthropic o Codex."
|
||||
"answer": "Sí. La app es gratuita y de código abierto, y puedes empezar con un modelo gratuito sin autenticación - sin registro, claves API ni tarjeta. Si quieres más modelos, conecta Claude, Codex, OpenCode/OpenRouter u otro proveedor compatible."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "¿Qué necesito para empezar?",
|
||||
"answer": "Solo instala la app - te guía en la detección del runtime y la autenticación del proveedor desde la interfaz. El onboarding sin configuración te pone en marcha en minutos."
|
||||
"answer": "Solo instala la app - empieza con el modelo gratuito sin autenticación, y conecta modelos con proveedor desde la interfaz solo cuando los necesites."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Gratis",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Todo incluido. Sin límites, sin claves API, sin tarjeta de crédito.",
|
||||
"description": "Empieza con el modelo gratuito sin autenticación incluido. Sin registro, sin clave API, sin tarjeta.",
|
||||
"features": [
|
||||
"Modelo gratuito sin autenticación para los primeros usos",
|
||||
"No necesitas cuenta ni login de proveedor para probar",
|
||||
"Acceso opcional a Claude, Codex y OpenCode",
|
||||
"Equipos de agentes ilimitados",
|
||||
"Tablero kanban con actualizaciones en tiempo real",
|
||||
"Revisión de código con vista diff",
|
||||
"Comunicación entre equipos",
|
||||
"Modo solo y equipo",
|
||||
"Monitorización de procesos en vivo",
|
||||
"Editor de código integrado",
|
||||
"Integración MCP"
|
||||
"Modo solo y equipo"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "C'est vraiment gratuit ?",
|
||||
"answer": "Oui. L'application est gratuite et open source. L'application n'a pas d'offre payante. Pour exécuter des agents, vous avez seulement besoin d'un accès à un provider/runtime pris en charge, comme Anthropic ou Codex."
|
||||
"answer": "Oui. L'application est gratuite et open source, et vous pouvez commencer avec un modèle gratuit sans authentification - sans inscription, clé API ni carte bancaire. Pour plus de modèles, connectez Claude, Codex, OpenCode/OpenRouter ou un autre provider pris en charge."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "De quoi ai-je besoin pour commencer ?",
|
||||
"answer": "Installez simplement l'application - elle vous guide pour la détection du runtime et l'authentification du provider depuis l'interface. L'onboarding zéro-configuration vous fait démarrer en quelques minutes."
|
||||
"answer": "Installez simplement l'application - commencez avec le modèle gratuit sans authentification, puis connectez les modèles provider-backed depuis l'interface seulement si nécessaire."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Gratuit",
|
||||
"price": "0€",
|
||||
"period": "",
|
||||
"description": "Tout inclus. Sans limites, sans clés API, sans carte bancaire.",
|
||||
"description": "Commencez avec le modèle gratuit sans authentification inclus. Sans inscription, sans clé API, sans carte bancaire.",
|
||||
"features": [
|
||||
"Modèle gratuit sans authentification pour les premiers essais",
|
||||
"Aucun compte ni login provider requis pour tester",
|
||||
"Accès optionnel à Claude, Codex et OpenCode",
|
||||
"Équipes d'agents illimitées",
|
||||
"Kanban avec mises à jour en temps réel",
|
||||
"Revue de code avec vue diff",
|
||||
"Communication inter-équipes",
|
||||
"Modes solo et équipe",
|
||||
"Surveillance des processus en direct",
|
||||
"Éditeur de code intégré",
|
||||
"Intégration MCP"
|
||||
"Modes solo et équipe"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "क्या यह सच में मुफ़्त है?",
|
||||
"answer": "हाँ। ऐप मुफ़्त और ओपन सोर्स है। ऐप का अपना कोई paid plan नहीं है। agents चलाने के लिए आपको सिर्फ़ किसी supported provider/runtime, जैसे Anthropic या Codex, का access चाहिए।"
|
||||
"answer": "हाँ। ऐप मुफ़्त और ओपन सोर्स है, और आप बिना registration, API key या credit card के मुफ़्त no-auth model से शुरू कर सकते हैं। अगर और models चाहिए, तो Claude, Codex, OpenCode/OpenRouter या कोई दूसरा supported provider जोड़ें।"
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "शुरू करने के लिए क्या चाहिए?",
|
||||
"answer": "बस ऐप इंस्टॉल करें - यह UI से runtime detection और provider authentication में गाइड करता है। zero-setup onboarding आपको कुछ ही मिनटों में शुरू करा देता है।"
|
||||
"answer": "बस ऐप इंस्टॉल करें - मुफ़्त no-auth model से शुरू करें, फिर provider-backed models को UI से तभी जोड़ें जब ज़रूरत हो।"
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "मुफ़्त",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "सब कुछ शामिल। कोई लिमिट नहीं, कोई API कुंजी नहीं, कोई क्रेडिट कार्ड नहीं।",
|
||||
"description": "शामिल मुफ़्त no-auth model से शुरू करें। कोई registration नहीं, कोई API key नहीं, कोई credit card नहीं।",
|
||||
"features": [
|
||||
"पहले runs के लिए मुफ़्त no-auth model",
|
||||
"Try करने के लिए account या provider login नहीं चाहिए",
|
||||
"Claude, Codex और OpenCode provider access optional",
|
||||
"असीमित एजेंट टीमें",
|
||||
"रियल-टाइम अपडेट के साथ कानबन बोर्ड",
|
||||
"diff व्यू के साथ कोड रिव्यू",
|
||||
"क्रॉस-टीम कम्युनिकेशन",
|
||||
"सोलो और टीम मोड",
|
||||
"लाइव प्रोसेस मॉनिटरिंग",
|
||||
"बिल्ट-इन कोड एडिटर",
|
||||
"MCP इंटीग्रेशन"
|
||||
"सोलो और टीम मोड"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "本当に無料ですか?",
|
||||
"answer": "はい。アプリは無料のオープンソースです。アプリ自体に有料プランはありません。エージェントを実行するには、Anthropic や Codex など対応する provider/runtime へのアクセスだけが必要です。"
|
||||
"answer": "はい。アプリは無料のオープンソースで、登録、APIキー、クレジットカードなしで認証なしの無料モデルから始められます。さらに多くのモデルが必要な場合は、Claude、Codex、OpenCode/OpenRouter など対応 provider を接続できます。"
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "始めるには何が必要ですか?",
|
||||
"answer": "アプリをインストールするだけです - UI 上で runtime の検出と provider 認証を案内します。ゼロ設定のオンボーディングで数分で開始できます。"
|
||||
"answer": "アプリをインストールするだけです - 認証なしの無料モデルから始め、provider-backed モデルは必要になった時だけ UI から接続できます。"
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "無料",
|
||||
"price": "¥0",
|
||||
"period": "",
|
||||
"description": "すべて含まれています。制限なし、APIキー不要、クレジットカード不要。",
|
||||
"description": "含まれている認証なしの無料モデルから開始できます。登録なし、APIキー不要、クレジットカード不要。",
|
||||
"features": [
|
||||
"最初の実行に使える認証なしの無料モデル",
|
||||
"試用にアカウントや provider ログインは不要",
|
||||
"Claude、Codex、OpenCode provider 接続は任意",
|
||||
"無制限のエージェントチーム",
|
||||
"リアルタイム更新のカンバンボード",
|
||||
"Diffビュー付きコードレビュー",
|
||||
"チーム間コミュニケーション",
|
||||
"ソロ&チームモード",
|
||||
"ライブプロセス監視",
|
||||
"組み込みコードエディタ",
|
||||
"MCP統合"
|
||||
"ソロ&チームモード"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "É realmente grátis?",
|
||||
"answer": "Sim. O app é gratuito e open source. O app não tem plano pago próprio. Para rodar agentes, você só precisa de acesso a um provider/runtime compatível, como Anthropic ou Codex."
|
||||
"answer": "Sim. O app é gratuito e open source, e você pode começar com um modelo gratuito sem autenticação - sem cadastro, chave de API ou cartão. Para mais modelos, conecte Claude, Codex, OpenCode/OpenRouter ou outro provedor compatível."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "O que preciso para começar?",
|
||||
"answer": "Basta instalar o app - ele guia a detecção do runtime e a autenticação do provider pela interface. O onboarding sem configuração coloca você em ação em minutos."
|
||||
"answer": "Basta instalar o app - comece com o modelo gratuito sem autenticação, depois conecte modelos com provedor pela interface só quando precisar."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Grátis",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Tudo incluído. Sem limites, sem chaves de API, sem cartão de crédito.",
|
||||
"description": "Comece com o modelo gratuito sem autenticação incluído. Sem cadastro, sem chave de API, sem cartão.",
|
||||
"features": [
|
||||
"Modelo gratuito sem autenticação para os primeiros usos",
|
||||
"Não precisa de conta nem login de provedor para testar",
|
||||
"Acesso opcional a Claude, Codex e OpenCode",
|
||||
"Equipes de agentes ilimitadas",
|
||||
"Quadro kanban com atualizações em tempo real",
|
||||
"Revisão de código com visualização diff",
|
||||
"Comunicação entre equipes",
|
||||
"Modo solo e equipe",
|
||||
"Monitoramento de processos ao vivo",
|
||||
"Editor de código integrado",
|
||||
"Integração MCP"
|
||||
"Modo solo e equipe"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "Это действительно бесплатно?",
|
||||
"answer": "Да. Само приложение полностью бесплатное и с открытым кодом. У него нет собственного платного тарифа. Для запуска агентов нужен только доступ к поддерживаемому провайдеру/рантайму, например Anthropic или Codex."
|
||||
"answer": "Да. Приложение бесплатное и open source, и можно начать с бесплатной модели без авторизации - без регистрации, API-ключей и карты. Если нужны дополнительные модели, подключите Claude, Codex, OpenCode/OpenRouter или другого поддерживаемого провайдера."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "Что нужно для начала?",
|
||||
"answer": "Просто установите приложение - оно проведёт вас через определение рантайма и аутентификацию провайдера прямо в интерфейсе. Онбординг без настройки запустит вас за минуты."
|
||||
"answer": "Просто установите приложение - начните с бесплатной модели без авторизации, а provider-backed модели подключайте из интерфейса только когда они понадобятся."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Бесплатно",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Всё включено. Без лимитов, без API-ключей, без кредитной карты.",
|
||||
"description": "Начните со встроенной бесплатной модели без авторизации. Без регистрации, API-ключей и карты.",
|
||||
"features": [
|
||||
"Бесплатная модель без авторизации для первого запуска",
|
||||
"Без аккаунта и логина провайдера для пробы",
|
||||
"Опциональный доступ к Claude, Codex и OpenCode",
|
||||
"Безлимитные команды агентов",
|
||||
"Канбан-доска с обновлениями в реальном времени",
|
||||
"Код-ревью с diff-просмотром",
|
||||
"Межкомандная коммуникация",
|
||||
"Соло и командный режимы",
|
||||
"Мониторинг живых процессов",
|
||||
"Встроенный редактор кода",
|
||||
"Интеграция MCP"
|
||||
"Соло и командный режимы"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "真的免费吗?",
|
||||
"answer": "是的。应用本身免费且开源。应用没有自己的付费方案。要运行智能体,你只需要接入受支持的 provider/runtime,例如 Anthropic 或 Codex。"
|
||||
"answer": "是的。应用免费且开源,你可以从无需认证的免费模型开始,无需注册、API 密钥或信用卡。如果需要更多模型,可以连接 Claude、Codex、OpenCode/OpenRouter 或其他受支持的提供商。"
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "开始需要什么?",
|
||||
"answer": "只需安装应用 - 它会在界面中引导你完成 runtime 检测和 provider 认证。零配置上手,几分钟即可运行。"
|
||||
"answer": "只需安装应用 - 先使用无需认证的免费模型,需要更多 provider-backed 模型时再从界面连接。"
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "免费",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "全部功能。无限制,无需 API 密钥,无需信用卡。",
|
||||
"description": "从内置无需认证的免费模型开始。无需注册、API 密钥或信用卡。",
|
||||
"features": [
|
||||
"用于首次运行的无需认证的免费模型",
|
||||
"试用无需账号或提供商登录",
|
||||
"可选连接 Claude、Codex 和 OpenCode 提供商",
|
||||
"无限智能体团队",
|
||||
"实时更新的看板",
|
||||
"带 diff 查看的代码审查",
|
||||
"跨团队通信",
|
||||
"单人和团队模式",
|
||||
"实时进程监控",
|
||||
"内置代码编辑器",
|
||||
"MCP 集成"
|
||||
"单人和团队模式"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@
|
|||
<style scoped>
|
||||
.app-layout__main {
|
||||
flex: 1;
|
||||
padding-top: 64px;
|
||||
background:
|
||||
radial-gradient(circle at 78% 18%, rgba(0, 234, 255, 0.08), transparent 32%),
|
||||
radial-gradient(circle at 18% 72%, rgba(47, 125, 255, 0.07), transparent 38%),
|
||||
linear-gradient(180deg, #02050d 0%, #050814 58%, #02050d 100%);
|
||||
}
|
||||
|
||||
.v-theme--light .app-layout__main {
|
||||
background:
|
||||
radial-gradient(circle at 52% 0%, rgba(255, 43, 255, 0.16), transparent 30%),
|
||||
radial-gradient(circle at 64% 0%, rgba(0, 234, 255, 0.14), transparent 28%),
|
||||
radial-gradient(circle at 78% 24%, rgba(113, 185, 255, 0.28), transparent 31%),
|
||||
radial-gradient(circle at 22% 72%, rgba(221, 170, 255, 0.24), transparent 38%),
|
||||
radial-gradient(circle at 8% 32%, rgba(101, 218, 255, 0.18), transparent 34%),
|
||||
linear-gradient(180deg, #edf8ff 0%, #eaf7fb 48%, #fbf7ff 100%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "أدوات قوية تجعل التعاون متعدد الوكلاء يعمل فعلاً."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "مجاني 100%. بدون شروط.",
|
||||
"sectionSubtitle": "مفتوح المصدر، بدون مفاتيح API، بدون إعدادات. فقط ثبّت وابدأ.",
|
||||
"sectionTitle": "التثبيت مجاني. نموذج مجاني مضمّن.",
|
||||
"sectionSubtitle": "ابدأ فوراً بنموذج مجاني بدون مصادقة - بدون حساب أو مفتاح API أو بطاقة. وصّل Claude أو Codex أو OpenCode/OpenRouter أو مزودين آخرين فقط عندما تحتاج نماذج إضافية.",
|
||||
"getStarted": "حمّل الآن",
|
||||
"popular": "مجاني",
|
||||
"note": "مفتوح المصدر. بدون مفاتيح API. بدون إعدادات. يعمل محلياً بالكامل."
|
||||
"freeModelCallout": "نموذج مجاني بدون مصادقة مضمّن",
|
||||
"note": "لا يملك Agent Teams أي خطة مدفوعة. النموذج المجاني بدون مصادقة يتيح التجربة فوراً؛ استخدام المزودين المدفوعين اختياري ويتبع المزود الذي تختاره."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "ماذا يقول المطورون",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - تنسيق وكلاء الذكاء الاصطناعي للمطورين",
|
||||
"homeDescription": "تطبيق سطح مكتب مجاني ومفتوح المصدر لتشكيل فرق وكلاء الذكاء الاصطناعي. لوحة كانبان، مراجعة الكود، تواصل بين الفرق. يعمل محلياً بالكامل.",
|
||||
"homeDescription": "تطبيق سطح مكتب مجاني ومفتوح المصدر لفرق وكلاء الذكاء الاصطناعي. ابدأ بنموذج مجاني بدون مصادقة، ثم وصّل Claude أو Codex أو OpenCode عند الحاجة.",
|
||||
"downloadTitle": "تنزيل Agent Teams لنظام macOS وWindows وLinux",
|
||||
"downloadDescription": "نزّل Agent Teams لنظام macOS وWindows وLinux. تطبيق سطح مكتب مجاني ومفتوح المصدر لفرق وكلاء Claude وCodex وOpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Leistungsstarke Tools für effektive Multi-Agenten-Zusammenarbeit."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Kostenlos. Ohne Haken.",
|
||||
"sectionSubtitle": "Open Source, keine API-Schlüssel, keine Konfiguration. Einfach installieren und loslegen.",
|
||||
"sectionTitle": "Kostenlos installieren. Kostenloses Modell inklusive.",
|
||||
"sectionSubtitle": "Starten Sie sofort mit einem kostenlosen Modell ohne Authentifizierung - ohne Konto, API-Schlüssel oder Kreditkarte. Verbinden Sie Claude, Codex, OpenCode/OpenRouter oder andere Provider nur, wenn Sie mehr Modelle möchten.",
|
||||
"getStarted": "Herunterladen",
|
||||
"popular": "Kostenlos",
|
||||
"note": "Open Source. Keine API-Schlüssel. Keine Konfiguration. Läuft vollständig lokal."
|
||||
"freeModelCallout": "Kostenloses Modell ohne Authentifizierung inklusive",
|
||||
"note": "Agent Teams hat keinen eigenen Bezahlplan. Mit dem kostenlosen Modell ohne Authentifizierung können Sie sofort testen; kostenpflichtige Provider-Nutzung ist optional und hängt vom gewählten Provider ab."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Was Entwickler sagen",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - KI-Agenten-Orchestrierung für Entwickler",
|
||||
"homeDescription": "Kostenlose Open-Source-Desktop-App für KI-Agenten-Teams. Kanban-Board, Code-Review, teamübergreifende Kommunikation. Läuft vollständig lokal.",
|
||||
"homeDescription": "Kostenlose Open-Source-Desktop-App für KI-Agenten-Teams. Starten Sie mit einem kostenlosen Modell ohne Authentifizierung und verbinden Sie Claude, Codex oder OpenCode bei Bedarf.",
|
||||
"downloadTitle": "Agent Teams für macOS, Windows und Linux herunterladen",
|
||||
"downloadDescription": "Laden Sie Agent Teams für macOS, Windows und Linux herunter. Kostenlose Open-Source-Desktop-App für Claude-, Codex- und OpenCode-Agententeams."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Powerful tools that make multi-agent collaboration actually work."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Free. No strings attached.",
|
||||
"sectionSubtitle": "Open source, no API keys, no configuration. Just install and go.",
|
||||
"sectionTitle": "Free to install. Free model included.",
|
||||
"sectionSubtitle": "Start immediately with a free model with no auth - no account, API key, or credit card. Connect Claude, Codex, OpenCode/OpenRouter, or other provider access only when you want more models.",
|
||||
"getStarted": "Download Now",
|
||||
"popular": "Free",
|
||||
"note": "Open source. No API keys. No configuration. Runs entirely locally."
|
||||
"freeModelCallout": "Free model with no auth included",
|
||||
"note": "Agent Teams has no paid tier. The free model with no auth lets you try it right away; paid provider usage is optional and controlled by the provider you choose."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "What developers say",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - AI Agent Orchestration for Developers",
|
||||
"homeDescription": "Free, open-source desktop app for assembling AI agent teams. Kanban board, code review, cross-team communication. Runs entirely locally.",
|
||||
"homeDescription": "Free, open-source desktop app for AI agent teams. Start with a free model with no auth, then connect Claude, Codex, or OpenCode when you need more models.",
|
||||
"downloadTitle": "Download Agent Teams for macOS, Windows, and Linux",
|
||||
"downloadDescription": "Download Agent Teams for macOS, Windows, and Linux. Free open-source desktop app for Claude, Codex, and OpenCode agent teams."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Herramientas potentes que hacen que la colaboración multi-agente realmente funcione."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Gratis. Sin letra pequeña.",
|
||||
"sectionSubtitle": "Código abierto, sin claves API, sin configuración. Instala y empieza.",
|
||||
"sectionTitle": "Gratis para instalar. Modelo gratis incluido.",
|
||||
"sectionSubtitle": "Empieza al instante con un modelo gratuito sin autenticación - sin cuenta, clave API ni tarjeta. Conecta Claude, Codex, OpenCode/OpenRouter u otros proveedores solo si quieres más modelos.",
|
||||
"getStarted": "Descargar ahora",
|
||||
"popular": "Gratis",
|
||||
"note": "Código abierto. Sin claves API. Sin configuración. Funciona completamente en local."
|
||||
"freeModelCallout": "Modelo gratuito sin autenticación incluido",
|
||||
"note": "Agent Teams no tiene plan de pago propio. El modelo gratuito sin autenticación te permite probarlo de inmediato; el uso de proveedores de pago es opcional y depende del proveedor que elijas."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Lo que dicen los desarrolladores",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Orquestación de agentes IA para desarrolladores",
|
||||
"homeDescription": "App de escritorio gratuita y de código abierto para montar equipos de agentes IA. Tablero kanban, revisión de código, comunicación entre equipos. Funciona completamente en local.",
|
||||
"homeDescription": "App de escritorio gratuita y open source para equipos de agentes IA. Empieza con un modelo gratuito sin autenticación, y conecta Claude, Codex u OpenCode cuando necesites más modelos.",
|
||||
"downloadTitle": "Descargar Agent Teams para macOS, Windows y Linux",
|
||||
"downloadDescription": "Descarga Agent Teams para macOS, Windows y Linux. App de escritorio gratis y open source para equipos de agentes Claude, Codex y OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Des outils puissants pour une collaboration multi-agents efficace."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Gratuit. Sans conditions.",
|
||||
"sectionSubtitle": "Open source, sans clé API, sans configuration. Installez et c'est parti.",
|
||||
"sectionTitle": "Installation gratuite. Modèle gratuit inclus.",
|
||||
"sectionSubtitle": "Commencez immédiatement avec un modèle gratuit sans authentification - sans compte, clé API ni carte bancaire. Connectez Claude, Codex, OpenCode/OpenRouter ou un autre provider seulement si vous voulez plus de modèles.",
|
||||
"getStarted": "Télécharger",
|
||||
"popular": "Gratuit",
|
||||
"note": "Open source. Sans clé API. Sans configuration. Fonctionne entièrement en local."
|
||||
"freeModelCallout": "Modèle gratuit sans authentification inclus",
|
||||
"note": "Agent Teams n'a pas d'offre payante. Le modèle gratuit sans authentification permet d'essayer tout de suite; l'usage de providers payants est optionnel et dépend du provider choisi."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Ce que disent les développeurs",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Orchestration d'agents IA pour développeurs",
|
||||
"homeDescription": "Application desktop gratuite et open source pour gérer des équipes d'agents IA. Tableau kanban, revue de code, communication inter-équipes. Fonctionne entièrement en local.",
|
||||
"homeDescription": "Application desktop gratuite et open source pour équipes d'agents IA. Commencez avec un modèle gratuit sans authentification, puis connectez Claude, Codex ou OpenCode si besoin.",
|
||||
"downloadTitle": "Télécharger Agent Teams pour macOS, Windows et Linux",
|
||||
"downloadDescription": "Téléchargez Agent Teams pour macOS, Windows et Linux. Application desktop gratuite et open source pour équipes d'agents Claude, Codex et OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "शक्तिशाली उपकरण जो मल्टी-एजेंट सहयोग को वास्तव में काम करते हैं।"
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% मुफ़्त। कोई शर्त नहीं।",
|
||||
"sectionSubtitle": "ओपन सोर्स, कोई API कुंजी नहीं, कोई कॉन्फ़िगरेशन नहीं। बस इंस्टॉल करें और शुरू करें।",
|
||||
"sectionTitle": "इंस्टॉल मुफ़्त। मुफ़्त मॉडल शामिल।",
|
||||
"sectionSubtitle": "मुफ़्त no-auth model से तुरंत शुरू करें - कोई account, API key या credit card नहीं। Claude, Codex, OpenCode/OpenRouter या अन्य providers तभी जोड़ें जब आपको और models चाहिए।",
|
||||
"getStarted": "अभी डाउनलोड करें",
|
||||
"popular": "मुफ़्त",
|
||||
"note": "ओपन सोर्स। कोई API कुंजी नहीं। कोई कॉन्फ़िगरेशन नहीं। पूरी तरह लोकल चलता है।"
|
||||
"freeModelCallout": "मुफ़्त no-auth model शामिल",
|
||||
"note": "Agent Teams का अपना कोई paid plan नहीं है। मुफ़्त no-auth model से आप तुरंत try कर सकते हैं; paid provider usage optional है और चुने गए provider पर निर्भर है।"
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "डेवलपर्स क्या कहते हैं",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - डेवलपर्स के लिए AI एजेंट ऑर्केस्ट्रेशन",
|
||||
"homeDescription": "AI एजेंट टीमें बनाने के लिए मुफ़्त, ओपन-सोर्स डेस्कटॉप ऐप। कानबन बोर्ड, कोड रिव्यू, क्रॉस-टीम संचार। पूरी तरह लोकल चलता है।",
|
||||
"homeDescription": "AI agent teams के लिए मुफ़्त open-source desktop app। मुफ़्त no-auth model से शुरू करें, फिर ज़रूरत पर Claude, Codex या OpenCode जोड़ें।",
|
||||
"downloadTitle": "macOS, Windows और Linux के लिए Agent Teams डाउनलोड करें",
|
||||
"downloadDescription": "macOS, Windows और Linux के लिए Agent Teams डाउनलोड करें। Claude, Codex और OpenCode एजेंट टीमों के लिए मुफ़्त ओपन-सोर्स डेस्कटॉप ऐप।"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "マルチエージェント連携を実現する強力なツール。"
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100%無料。制約なし。",
|
||||
"sectionSubtitle": "オープンソース、APIキー不要、設定不要。インストールするだけ。",
|
||||
"sectionTitle": "インストール無料。無料モデル付き。",
|
||||
"sectionSubtitle": "認証なしの無料モデルですぐに開始できます - アカウント、APIキー、クレジットカードは不要。追加モデルが必要な時だけ Claude、Codex、OpenCode/OpenRouter などを接続できます。",
|
||||
"getStarted": "ダウンロード",
|
||||
"popular": "無料",
|
||||
"note": "オープンソース。APIキー不要。設定不要。完全にローカルで動作。"
|
||||
"freeModelCallout": "認証なしの無料モデルが含まれています",
|
||||
"note": "Agent Teams 自体に有料プランはありません。認証なしの無料モデルですぐに試せます。有料 provider の利用は任意で、選択した provider の条件に従います。"
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "開発者の声",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - 開発者向けAIエージェントオーケストレーション",
|
||||
"homeDescription": "AIエージェントチームを管理する無料のオープンソースデスクトップアプリ。カンバンボード、コードレビュー、チーム間通信。完全にローカルで動作。",
|
||||
"homeDescription": "AIエージェントチーム向けの無料オープンソースデスクトップアプリ。認証なしの無料モデルから始め、必要に応じて Claude、Codex、OpenCode を接続できます。",
|
||||
"downloadTitle": "macOS、Windows、Linux向けAgent Teamsをダウンロード",
|
||||
"downloadDescription": "macOS、Windows、Linux向けAgent Teamsをダウンロード。Claude、Codex、OpenCodeエージェントチーム用の無料オープンソースデスクトップアプリ。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Ferramentas poderosas que fazem a colaboração multi-agente realmente funcionar."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Grátis. Sem pegadinhas.",
|
||||
"sectionSubtitle": "Código aberto, sem chaves de API, sem configuração. Instale e comece.",
|
||||
"sectionTitle": "Grátis para instalar. Modelo grátis incluído.",
|
||||
"sectionSubtitle": "Comece na hora com um modelo gratuito sem autenticação - sem conta, chave de API ou cartão. Conecte Claude, Codex, OpenCode/OpenRouter ou outros provedores só quando quiser mais modelos.",
|
||||
"getStarted": "Baixar agora",
|
||||
"popular": "Grátis",
|
||||
"note": "Código aberto. Sem chaves de API. Sem configuração. Roda totalmente local."
|
||||
"freeModelCallout": "Modelo gratuito sem autenticação incluído",
|
||||
"note": "Agent Teams não tem plano pago próprio. O modelo gratuito sem autenticação permite testar imediatamente; uso de provedores pagos é opcional e depende do provedor escolhido."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "O que os desenvolvedores dizem",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Orquestração de agentes IA para desenvolvedores",
|
||||
"homeDescription": "App desktop gratuito e open source para montar equipes de agentes IA. Quadro kanban, revisão de código, comunicação entre equipes. Roda totalmente local.",
|
||||
"homeDescription": "App desktop gratuito e open source para equipes de agentes IA. Comece com um modelo gratuito sem autenticação, depois conecte Claude, Codex ou OpenCode quando precisar.",
|
||||
"downloadTitle": "Baixar Agent Teams para macOS, Windows e Linux",
|
||||
"downloadDescription": "Baixe o Agent Teams para macOS, Windows e Linux. App desktop gratuito e open source para equipes de agentes Claude, Codex e OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Мощные инструменты, которые делают мультиагентную совместную работу реальностью."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Бесплатно. Без подвоха.",
|
||||
"sectionSubtitle": "Открытый код, без API-ключей, без конфигурации. Просто установите и работайте.",
|
||||
"sectionTitle": "Бесплатно установить. Бесплатная модель уже внутри.",
|
||||
"sectionSubtitle": "Можно сразу начать с бесплатной модели без авторизации - без регистрации, API-ключей и карты. Claude, Codex, OpenCode/OpenRouter и другие провайдеры подключайте только если нужны дополнительные модели.",
|
||||
"getStarted": "Скачать",
|
||||
"popular": "Бесплатно",
|
||||
"note": "Открытый код. Без API-ключей. Без конфигурации. Работает полностью локально."
|
||||
"freeModelCallout": "Бесплатная модель без авторизации включена",
|
||||
"note": "У Agent Teams нет платного тарифа. Бесплатная модель без авторизации даёт попробовать сразу; платные провайдеры опциональны и зависят от выбранного сервиса."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Что говорят разработчики",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Оркестрация ИИ-агентов для разработчиков",
|
||||
"homeDescription": "Бесплатное десктопное приложение с открытым кодом для управления командами ИИ-агентов. Канбан-доска, код-ревью, межкомандная коммуникация. Работает полностью локально.",
|
||||
"homeDescription": "Бесплатное open-source desktop-приложение для команд ИИ-агентов. Начните с бесплатной модели без авторизации, а Claude, Codex или OpenCode подключайте когда нужны дополнительные модели.",
|
||||
"downloadTitle": "Скачать Agent Teams для macOS, Windows и Linux",
|
||||
"downloadDescription": "Скачайте Agent Teams для macOS, Windows и Linux. Бесплатное open-source приложение для команд агентов Claude, Codex и OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "强大的工具,让多智能体协作真正有效。"
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% 免费,没有附加条件。",
|
||||
"sectionSubtitle": "开源,无需 API 密钥,无需配置。安装即用。",
|
||||
"sectionTitle": "免费安装。内置免费模型。",
|
||||
"sectionSubtitle": "可立即使用无需认证的免费模型 - 无需账号、API 密钥或信用卡。只有在需要更多模型时,才连接 Claude、Codex、OpenCode/OpenRouter 或其他提供商。",
|
||||
"getStarted": "立即下载",
|
||||
"popular": "免费",
|
||||
"note": "开源。无需 API 密钥。无需配置。完全本地运行。"
|
||||
"freeModelCallout": "已包含无需认证的免费模型",
|
||||
"note": "Agent Teams 没有自己的付费套餐。无需认证的免费模型可让你立即试用;付费提供商的使用是可选的,并由你选择的提供商控制。"
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "开发者怎么说",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - 面向开发者的 AI 智能体编排",
|
||||
"homeDescription": "免费开源桌面应用,用于组建 AI 智能体团队。看板、代码审查、跨团队通信。完全本地运行。",
|
||||
"homeDescription": "面向 AI 智能体团队的免费开源桌面应用。先使用无需认证的免费模型,需要更多模型时再连接 Claude、Codex 或 OpenCode。",
|
||||
"downloadTitle": "下载适用于 macOS、Windows 和 Linux 的 Agent Teams",
|
||||
"downloadDescription": "下载适用于 macOS、Windows 和 Linux 的 Agent Teams。面向 Claude、Codex 和 OpenCode 智能体团队的免费开源桌面应用。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Agent Teams is distributed as a desktop app for macOS, Windows, and Linux.
|
|||
|
||||
::: tip Shortest path
|
||||
1. Download the build for your platform below
|
||||
2. Launch the app — it detects runtimes and guides provider auth from the UI
|
||||
2. Launch the app - start with the free model with no auth or connect provider auth from the UI
|
||||
3. Start the [quickstart](/guide/quickstart) to create your first team
|
||||
|
||||
Desktop app startup: run `pnpm dev` for the Electron app. Do not start the browser/web dev mode for normal use.
|
||||
|
|
@ -30,16 +30,16 @@ Unsigned or newly published open-source apps can trigger SmartScreen. If you tru
|
|||
|
||||
## Requirements
|
||||
|
||||
The packaged app is designed for zero-setup onboarding. It guides you through runtime detection and provider authentication from the UI — no manual CLI configuration needed.
|
||||
The packaged app is designed for zero-setup onboarding. You can start with the free model with no auth - no registration, API keys, or credit card. If you want more models, the app guides runtime detection and provider authentication from the UI.
|
||||
|
||||
To use agent runtimes, you need access to at least one provider:
|
||||
For paid or account-backed models, connect at least one provider:
|
||||
|
||||
| Provider | Access method |
|
||||
| ------------------ | ------------------------------------------------- |
|
||||
| Claude (Anthropic) | Claude Code CLI login or API key |
|
||||
| Codex (OpenAI) | Codex CLI login or API key |
|
||||
| Gemini (Google) | Google ADC, Gemini CLI, or API key |
|
||||
| OpenCode | API key for a supported backend (e.g. OpenRouter) |
|
||||
| OpenCode | Included free model with no auth, or API key for a supported backend (e.g. OpenRouter) |
|
||||
|
||||
::: info
|
||||
Gemini is available as a supported provider path. See [Providers and runtimes](/reference/providers-runtimes) for auth options and current status across all providers.
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ For project conventions and architecture guidance, refer to these canonical file
|
|||
|
||||
## 1. Run from source or download
|
||||
|
||||
**Download the packaged app** for macOS, Windows, or Linux from the <a href="/download/" target="_self">download page</a> — no prerequisites needed. The app guides runtime detection and provider authentication from the UI.
|
||||
**Download the packaged app** for macOS, Windows, or Linux from the <a href="/download/" target="_self">download page</a> - no prerequisites needed. Start with the free model with no auth, or connect provider auth from the UI when you want more models.
|
||||
|
||||
**Or run from source** for development:
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ The setup flow auto-detects installed runtimes on your machine. A common first s
|
|||
| -------- | ----------------------------------------------- |
|
||||
| Claude | Claude Code users and existing Anthropic access |
|
||||
| Codex | Codex-native workflows and OpenAI access |
|
||||
| OpenCode | Multi-model teams and many provider backends |
|
||||
| OpenCode | Free model with no auth, multi-model teams, and many provider backends |
|
||||
|
||||
::: info
|
||||
Gemini is available as a supported provider path. See [Providers and runtimes](/reference/providers-runtimes) for auth options and current provider status.
|
||||
|
|
@ -92,7 +92,7 @@ Gemini is available as a supported provider path. See [Providers and runtimes](/
|
|||
|
||||
See [Runtime setup](/guide/runtime-setup) for detailed configuration per provider.
|
||||
|
||||
To verify the selected runtime outside the app, check the binary and test auth:
|
||||
To verify a paid or account-backed runtime outside the app, check the binary and test auth:
|
||||
|
||||
```bash
|
||||
# Check that the runtime is installed and on PATH
|
||||
|
|
@ -101,7 +101,7 @@ command -v codex && codex --version
|
|||
command -v opencode && opencode --version
|
||||
```
|
||||
|
||||
If the command fails, fix the runtime installation or `PATH` first. Team prompts cannot work around a missing binary or missing provider auth.
|
||||
If the command fails, fix the runtime installation or `PATH` first. Team prompts cannot work around a missing binary or missing provider auth for models that require it.
|
||||
|
||||
::: tip
|
||||
If the binary is found but the app reports "not logged in", the environment may differ between your terminal and the app. See the [auth diagnostic log](/guide/troubleshooting#auth-diagnostic-log) to compare them.
|
||||
|
|
@ -162,7 +162,7 @@ Before approving the first task, check three things:
|
|||
| Symptom | Likely cause | Check |
|
||||
| --- | --- | --- |
|
||||
| App does not detect a runtime | Binary not on `PATH`, or app and terminal see different environments | Run `command -v <runtime>` in a terminal, then use the same terminal env to launch the app |
|
||||
| Team launch hangs | Missing provider auth, wrong model string, or runtime binary not found | See [Troubleshooting](/guide/troubleshooting#team-does-not-launch) |
|
||||
| Team launch hangs | Missing provider auth for a paid/account model, wrong model string, or runtime binary not found | See [Troubleshooting](/guide/troubleshooting#team-does-not-launch) |
|
||||
| OpenCode lane stuck on `registered` | Lane evidence not committed yet, or model string mismatch | Inspect `~/.claude/teams/<team>/.opencode-runtime/lanes/` |
|
||||
| Agent replies missing | Runtime delivery retry, parsing, or task attribution issue | Open task logs and check the delivery ledger |
|
||||
| Provider returns 429s | Rate limit reached | Wait for reset or switch model/provider |
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Agent Teams is a coordination layer. The actual model work runs through supporte
|
|||
| --- | --- |
|
||||
| Already use Claude Code or have Anthropic access | **Claude** - familiar auth, minimal setup |
|
||||
| Use Codex or OpenAI-based workflows | **Codex** - native integration |
|
||||
| Want to try Agent Teams without signup or API keys | **OpenCode** - use the included free model with no auth |
|
||||
| Want multi-model routing or broad provider coverage | **OpenCode** - most flexible, one config for many backends |
|
||||
| Are not sure which runtime fits | **OpenCode** - covers the most provider options and lets you switch later |
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ Start with one runtime and one teammate. Confirm one launch works before expandi
|
|||
Before launching a team, make sure:
|
||||
|
||||
- The runtime binary is installed and on your `PATH`.
|
||||
- Your provider account has active access to the model you intend to use.
|
||||
- Your provider account has active access to the model you intend to use, unless you start with the included free OpenCode model with no auth.
|
||||
- The project path exists and is readable.
|
||||
- The app and your terminal use the same home/config environment when you test auth manually.
|
||||
|
||||
|
|
@ -55,10 +56,10 @@ Gemini is available as a supported provider path with Google ADC (`gcloud auth`)
|
|||
|
||||
## Provider access
|
||||
|
||||
Agent Teams has no paid tier of its own. You bring the provider access you already have: subscriptions, local runtime auth, or API keys depending on the path you choose.
|
||||
Agent Teams has no paid tier of its own. You can start with the included free OpenCode model with no auth - no registration, API keys, or credit card. For additional models, bring the provider access you already have: subscriptions, local runtime auth, or API keys depending on the path you choose.
|
||||
|
||||
- **Claude** and **Codex** paths rely on their respective CLI auth tools.
|
||||
- **OpenCode** needs provider-specific API keys in a config file (e.g., `openrouter`, `openai`, `anthropic`).
|
||||
- **OpenCode** can run the included free model with no auth first. Other OpenCode models may need provider-specific API keys in a config file (e.g., `openrouter`, `openai`, `anthropic`).
|
||||
|
||||
## Auth configuration
|
||||
|
||||
|
|
@ -96,7 +97,7 @@ Codex-native launches use Codex account state and model catalog data when availa
|
|||
|
||||
### OpenCode
|
||||
|
||||
Create or edit `~/.opencode/config.json` (or the equivalent path on your platform) with the provider key you want:
|
||||
To use the included free model with no auth, select it in the app and launch without provider signup. To use other OpenCode backends, create or edit `~/.opencode/config.json` (or the equivalent path on your platform) with the provider key you want:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Recommended patterns:
|
|||
|
||||
## Provider costs
|
||||
|
||||
Agent Teams is free and open source. Provider usage is governed by the runtime/provider you select: subscription limits, API keys, account auth, rate limits, and provider policies all remain external to the app.
|
||||
Agent Teams is free and open source. You can start with the included free model with no auth - no registration, API keys, or credit card. Paid or account-backed provider usage is governed by the runtime/provider you select: subscription limits, API keys, account auth, rate limits, and provider policies all remain external to the app.
|
||||
|
||||
## Capability checks
|
||||
|
||||
|
|
|
|||
|
|
@ -23,16 +23,16 @@ Agent Teams распространяется как desktop-приложение
|
|||
|
||||
## Требования
|
||||
|
||||
Пакетная сборка рассчитана на zero-setup onboarding. Приложение само помогает с runtime detection и provider authentication — ручная настройка CLI не нужна.
|
||||
Пакетная сборка рассчитана на zero-setup onboarding. Можно начать с бесплатной модели без авторизации - без регистрации, API-ключей и карты. Если нужны дополнительные модели, приложение само помогает с runtime detection и provider authentication.
|
||||
|
||||
Для работы агентных рантаймов нужен доступ хотя бы к одному провайдеру:
|
||||
Для платных или account-backed моделей подключите хотя бы один провайдер:
|
||||
|
||||
| Провайдер | Способ доступа |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| Claude (Anthropic) | Claude Code CLI login или API key |
|
||||
| Codex (OpenAI) | Codex CLI login или API key |
|
||||
| Gemini (Google) | Google ADC, Gemini CLI или API key |
|
||||
| OpenCode | API key для поддерживаемого бэкенда (например, OpenRouter) |
|
||||
| OpenCode | Встроенная бесплатная модель без авторизации или API key для поддерживаемого бэкенда (например, OpenRouter) |
|
||||
|
||||
::: info
|
||||
Gemini — поддерживаемый провайдер. Варианты auth смотрите в разделе [Провайдеры и рантаймы](/ru/reference/providers-runtimes).
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ lang: ru-RU
|
|||
|
||||
- **macOS, Windows или Linux** машина
|
||||
- **Git-репозиторий** в качестве проекта (рекомендуется для diff review и worktree isolation)
|
||||
- Доступ хотя бы к одному провайдеру: Anthropic (Claude), OpenAI (Codex), OpenRouter (OpenCode) или Google (Gemini)
|
||||
- Бесплатная модель без авторизации для первого запуска или доступ к провайдеру, если нужны дополнительные модели: Anthropic (Claude), OpenAI (Codex), OpenRouter (OpenCode) или Google (Gemini)
|
||||
- Node.js 20+ и pnpm 10+ при запуске из исходников
|
||||
|
||||
Подробности и ссылки для скачивания — в разделе [Установка](/ru/guide/installation).
|
||||
|
|
@ -24,7 +24,7 @@ lang: ru-RU
|
|||
Скачайте последний релиз под вашу платформу на <a href="/ru/download/" target="_self">странице загрузок</a> или в [GitHub releases](https://github.com/777genius/agent-teams-ai/releases).
|
||||
|
||||
::: tip
|
||||
Приложение бесплатное и с открытым кодом. Выбранный runtime может требовать доступ к провайдеру — подробности в разделе [Установка](/ru/guide/installation).
|
||||
Приложение бесплатное и с открытым кодом. Можно начать с бесплатной модели без авторизации - без регистрации; дополнительные runtime/provider paths могут требовать доступ к провайдеру. Подробности в разделе [Установка](/ru/guide/installation).
|
||||
:::
|
||||
|
||||
::: info
|
||||
|
|
@ -55,7 +55,7 @@ git status --short
|
|||
| -------- | ------------------------------------------------------------------- |
|
||||
| Claude | Если вы уже используете Claude Code или у вас есть Anthropic access |
|
||||
| Codex | Для Codex-native workflows и OpenAI access |
|
||||
| OpenCode | Для multi-model команд и большого числа provider backends |
|
||||
| OpenCode | Бесплатная модель без авторизации, multi-model команды и много provider backends |
|
||||
|
||||
::: info
|
||||
Gemini — поддерживаемый провайдер. Варианты auth смотрите в разделе [Провайдеры и рантаймы](/ru/reference/providers-runtimes).
|
||||
|
|
@ -63,7 +63,7 @@ Gemini — поддерживаемый провайдер. Варианты aut
|
|||
|
||||
Подробная настройка каждого провайдера — в разделе [Настройка рантайма](/ru/guide/runtime-setup).
|
||||
|
||||
Чтобы проверить выбранный runtime вне приложения, запустите соответствующую команду версии:
|
||||
Чтобы проверить платный или account-backed runtime вне приложения, запустите соответствующую команду версии:
|
||||
|
||||
```bash
|
||||
claude --version
|
||||
|
|
@ -71,7 +71,7 @@ codex --version
|
|||
opencode --version
|
||||
```
|
||||
|
||||
Если команда падает в терминале, сначала исправьте runtime installation или `PATH`. Team prompts не смогут компенсировать отсутствующий binary или provider auth.
|
||||
Если команда падает в терминале, сначала исправьте runtime installation или `PATH`. Team prompts не смогут компенсировать отсутствующий binary или provider auth для моделей, которым он нужен.
|
||||
|
||||
Также можно проверить, что бинарник доступен в `PATH`:
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ Lead создаёт задачи, назначает работу и коорд
|
|||
| Симптом | Вероятная причина | Что проверить |
|
||||
| --- | --- | --- |
|
||||
| Приложение не видит runtime | Бинарник не в `PATH` или разные окружения у приложения и терминала | Запустите `command -v <runtime>` в терминале |
|
||||
| Запуск команды зависает | Нет provider auth, неверная модель или runtime не найден | Раздел [Диагностика](/ru/guide/troubleshooting#team-does-not-launch) |
|
||||
| Запуск команды зависает | Нет provider auth для платной/account модели, неверная модель или runtime не найден | Раздел [Диагностика](/ru/guide/troubleshooting#team-does-not-launch) |
|
||||
| OpenCode lane в статусе `registered` | Lane evidence ещё не зафиксирован или несовпадение модели | Проверьте `~/.claude/teams/<team>/.opencode-runtime/lanes/` |
|
||||
| Ответы агента не приходят | Runtime delivery retry, parsing или task attribution | Откройте task logs и проверьте delivery ledger |
|
||||
| Провайдер возвращает 429 | Достигнут лимит запросов | Дождитесь сброса или смените модель/провайдера |
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Agent Teams - координационный слой. Работа моделе
|
|||
Перед запуском команды убедитесь, что:
|
||||
|
||||
- Runtime binary установлен и находится в `PATH`.
|
||||
- Ваш аккаунт провайдера имеет доступ к выбранной модели.
|
||||
- Ваш аккаунт провайдера имеет доступ к выбранной модели, если вы не начинаете со встроенной OpenCode-модели без авторизации.
|
||||
- Путь к проекту существует и доступен для чтения.
|
||||
- Приложение и терминал используют одинаковое home/config окружение, когда вы вручную проверяете auth.
|
||||
|
||||
|
|
@ -45,10 +45,10 @@ Gemini — поддерживаемый провайдер с Google ADC (`gclou
|
|||
|
||||
## Доступ к провайдеру
|
||||
|
||||
У Agent Teams нет своего платного тарифа. Вы используете доступ к провайдеру, который у вас уже есть: подписка, локальная авторизация рантайма или API-ключи в зависимости от выбранного пути.
|
||||
У Agent Teams нет своего платного тарифа. Можно начать со встроенной OpenCode-модели без авторизации - без регистрации, API-ключей и карты. Для дополнительных моделей используйте доступ к провайдеру, который у вас уже есть: подписка, локальная авторизация рантайма или API-ключи в зависимости от выбранного пути.
|
||||
|
||||
- Для **Claude** и **Codex** используется auth соответствующего CLI.
|
||||
- Для **OpenCode** требуются provider-specific API keys в файле конфигурации (например, `openrouter`, `openai`, `anthropic`).
|
||||
- **OpenCode** может сначала работать через встроенную бесплатную модель без авторизации. Другие OpenCode-модели могут требовать provider-specific API keys в файле конфигурации (например, `openrouter`, `openai`, `anthropic`).
|
||||
|
||||
## Настройка авторизации
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ Codex-native launches используют Codex account state и model catalog
|
|||
|
||||
### OpenCode
|
||||
|
||||
Создайте или отредактируйте `~/.opencode/config.json` (или эквивалентный путь на вашей платформе):
|
||||
Для встроенной бесплатной модели без авторизации достаточно выбрать её в приложении и запустить без регистрации у провайдера. Для других OpenCode backend создайте или отредактируйте `~/.opencode/config.json` (или эквивалентный путь на вашей платформе):
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ Contributor-facing границы и canonical implementation guidance смот
|
|||
|
||||
## Стоимость providers
|
||||
|
||||
Agent Teams бесплатен и open source. Provider usage зависит от выбранного runtime/provider: subscription limits, API keys, account auth, rate limits и provider policies остаются внешними для приложения.
|
||||
Agent Teams бесплатен и open source. Можно начать со встроенной бесплатной модели без авторизации - без регистрации, API-ключей и карты. Платный или account-backed provider usage зависит от выбранного runtime/provider: subscription limits, API keys, account auth, rate limits и provider policies остаются внешними для приложения.
|
||||
|
||||
## Capability checks
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:workspace": "pnpm typecheck && pnpm --filter agent-teams-mcp typecheck && pnpm --filter agent-teams-mcp typecheck:test",
|
||||
"lint": "eslint src/ --cache --cache-location .eslintcache --cache-strategy content",
|
||||
"lint:fast": "eslint --config eslint.fast.config.js --cache --cache-location .eslintcache-fast --cache-strategy content src/",
|
||||
"lint:fast:files": "eslint --config eslint.fast.config.js --cache --cache-location .eslintcache-fast --cache-strategy content",
|
||||
"lint:mcp": "pnpm --filter agent-teams-mcp lint",
|
||||
"lint:fix": "eslint src/ --fix --cache --cache-location .eslintcache --cache-strategy content",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
|
||||
|
|
@ -244,7 +246,7 @@
|
|||
},
|
||||
"build": {
|
||||
"appId": "com.agent-teams.app",
|
||||
"productName": "Agent Teams UI",
|
||||
"productName": "Agent Teams AI",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
* NEW — not from agent-flow. Custom renderer for our task nodes.
|
||||
*/
|
||||
|
||||
import type { GraphNode } from '../ports/types';
|
||||
import { COLORS, getTaskStatusColor, getReviewStateColor } from '../constants/colors';
|
||||
import { TASK_PILL, MIN_VISIBLE_OPACITY, ANIM } from '../constants/canvas-constants';
|
||||
import { truncateText, wrapTextLines } from './draw-misc';
|
||||
import { drawPillShell, drawPillStackLayer } from './draw-pill-shell';
|
||||
import { ANIM, KANBAN_ZONE, MIN_VISIBLE_OPACITY, TASK_PILL } from '../constants/canvas-constants';
|
||||
import { COLORS, getReviewStateColor, getTaskStatusColor } from '../constants/colors';
|
||||
|
||||
import { wrapTextLines } from './draw-misc';
|
||||
import { drawPillShell } from './draw-pill-shell';
|
||||
import { hexWithAlpha } from './render-cache';
|
||||
|
||||
import type { KanbanZoneInfo } from '../layout/kanbanLayout';
|
||||
import type { GraphNode } from '../ports/types';
|
||||
|
||||
const KANBAN_HEADER_FONT = '600 10px monospace';
|
||||
const KANBAN_HEADER_ALPHA = 0.92;
|
||||
|
|
@ -82,7 +84,7 @@ function drawTaskPill(
|
|||
ctx.translate(x, y);
|
||||
|
||||
if (node.isOverflowStack) {
|
||||
drawOverflowStack(ctx, halfW, halfH, r, node, isSelected, isHovered);
|
||||
drawOverflowStack(ctx, halfW, r, node, time, isSelected, isHovered);
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
|
|
@ -200,8 +202,8 @@ function drawTaskPill(
|
|||
|
||||
// Comment count badge — on the bottom-right border edge, 1.5x bigger
|
||||
if (node.totalCommentCount && node.totalCommentCount > 0) {
|
||||
const badgeX = halfW - 6;
|
||||
const badgeY = halfH;
|
||||
const badgeX = halfW - 36;
|
||||
const badgeY = halfH - 30;
|
||||
|
||||
// Speech bubble background
|
||||
const bw = 20;
|
||||
|
|
@ -265,7 +267,7 @@ function drawTaskPillLod(
|
|||
ctx.translate(x, y);
|
||||
|
||||
if (node.isOverflowStack) {
|
||||
drawOverflowStack(ctx, halfW, halfH, r, node, isSelected, isHovered);
|
||||
drawOverflowStack(ctx, halfW, r, node, time, isSelected, isHovered);
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
|
|
@ -336,52 +338,41 @@ function drawLiveTaskLogIndicator(
|
|||
function drawOverflowStack(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
halfW: number,
|
||||
halfH: number,
|
||||
r: number,
|
||||
node: GraphNode,
|
||||
time: number,
|
||||
isSelected: boolean,
|
||||
isHovered: boolean
|
||||
): void {
|
||||
for (const [offset, alpha] of [
|
||||
[6, 0.18],
|
||||
[3, 0.28],
|
||||
] as const) {
|
||||
drawPillStackLayer(ctx, {
|
||||
width: TASK_PILL.width,
|
||||
height: TASK_PILL.height,
|
||||
radius: r,
|
||||
offsetX: offset,
|
||||
offsetY: -offset,
|
||||
fillColor: '#334155',
|
||||
fillAlpha: alpha,
|
||||
});
|
||||
}
|
||||
const footerHeight = KANBAN_ZONE.overflowHeight;
|
||||
|
||||
drawPillShell(ctx, {
|
||||
width: TASK_PILL.width,
|
||||
height: TASK_PILL.height,
|
||||
radius: r,
|
||||
height: footerHeight,
|
||||
radius: Math.min(r, footerHeight / 2),
|
||||
fillStyle: isSelected
|
||||
? COLORS.cardBgSelected
|
||||
: isHovered
|
||||
? 'rgba(15, 20, 40, 0.78)'
|
||||
: COLORS.cardBg,
|
||||
? 'rgba(12, 20, 40, 0.78)'
|
||||
: 'rgba(8, 14, 28, 0.64)',
|
||||
borderColor: node.isBlocked
|
||||
? hexWithAlpha(COLORS.edgeBlocking, isSelected ? 0.85 : 0.65)
|
||||
: hexWithAlpha(COLORS.taskPending, isSelected ? 0.85 : 0.55),
|
||||
borderWidth: node.isBlocked ? (isSelected ? 2.4 : 1.5) : isSelected ? 2 : 1,
|
||||
: isSelected
|
||||
? hexWithAlpha(COLORS.holoBright, 0.45)
|
||||
: 'rgba(255, 255, 255, 0.10)',
|
||||
borderWidth: node.isBlocked ? (isSelected ? 2.4 : 1.5) : isSelected ? 1.5 : 1,
|
||||
accentColor: node.isBlocked ? hexWithAlpha(COLORS.edgeBlocking, 0.6) : undefined,
|
||||
});
|
||||
|
||||
ctx.font = 'bold 12px sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.font = '600 11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = COLORS.textPrimary;
|
||||
ctx.fillText(node.label, -halfW + 14, -8);
|
||||
ctx.fillStyle = '#cbd5e1';
|
||||
ctx.fillText(node.label, 0, 0.5);
|
||||
|
||||
ctx.font = '10px monospace';
|
||||
ctx.fillStyle = COLORS.textDim;
|
||||
ctx.fillText('more tasks', -halfW + 14, 12);
|
||||
if (node.hasLiveTaskLogs) {
|
||||
drawLiveTaskLogIndicator(ctx, -halfW + 16, 0, time, true);
|
||||
}
|
||||
}
|
||||
|
||||
function drawReviewChip(
|
||||
|
|
@ -451,6 +442,24 @@ function drawCenteredSpacedText(
|
|||
ctx.textAlign = previousAlign;
|
||||
}
|
||||
|
||||
function drawLeftSpacedText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
letterSpacing: number
|
||||
): void {
|
||||
const chars = Array.from(text);
|
||||
const previousAlign = ctx.textAlign;
|
||||
ctx.textAlign = 'left';
|
||||
let cursorX = x;
|
||||
for (const char of chars) {
|
||||
ctx.fillText(char, cursorX, y);
|
||||
cursorX += ctx.measureText(char).width + letterSpacing;
|
||||
}
|
||||
ctx.textAlign = previousAlign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw kanban column headers above task columns.
|
||||
*/
|
||||
|
|
@ -470,41 +479,21 @@ export function drawColumnHeaders(
|
|||
const labelY = (zone.headers[0]?.y ?? zone.ownerY + 60) + 10;
|
||||
drawCenteredSpacedText(ctx, 'Unassigned', zone.ownerX, labelY, KANBAN_HEADER_LETTER_SPACING);
|
||||
|
||||
// Overflow badge
|
||||
for (const header of zone.headers) {
|
||||
if (header.overflowCount > 0) {
|
||||
ctx.font = '7px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillStyle = hexWithAlpha(header.color, 0.45);
|
||||
ctx.fillText(`+${header.overflowCount} more`, header.x, header.overflowY + 4);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const header of zone.headers) {
|
||||
ctx.font = KANBAN_HEADER_FONT;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillStyle = hexWithAlpha(header.color, KANBAN_HEADER_ALPHA);
|
||||
drawCenteredSpacedText(
|
||||
drawLeftSpacedText(
|
||||
ctx,
|
||||
header.label,
|
||||
header.x,
|
||||
header.x - TASK_PILL.width / 2 + 4,
|
||||
header.y + 10,
|
||||
KANBAN_HEADER_LETTER_SPACING
|
||||
);
|
||||
|
||||
// Overflow badge: "+N more"
|
||||
if (header.overflowCount > 0) {
|
||||
const badgeText = `+${header.overflowCount} more`;
|
||||
ctx.font = '10px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillStyle = hexWithAlpha(header.color, 0.45);
|
||||
ctx.fillText(badgeText, header.x, header.overflowY + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
* Adapted from agent-flow's hit-detection.ts (Apache 2.0).
|
||||
*/
|
||||
|
||||
import type { GraphEdge, GraphNode } from '../ports/types';
|
||||
import { BEAM, NODE, TASK_PILL, HIT_DETECTION } from '../constants/canvas-constants';
|
||||
import { BEAM, HIT_DETECTION, KANBAN_ZONE, NODE, TASK_PILL } from '../constants/canvas-constants';
|
||||
|
||||
import { bezierPoint, computeControlPoints } from './draw-edges';
|
||||
|
||||
import type { GraphEdge, GraphNode } from '../ports/types';
|
||||
|
||||
/**
|
||||
* Find the node at the given world-space coordinates.
|
||||
* Returns node ID or null.
|
||||
|
|
@ -39,7 +41,8 @@ export function findNodeAt(
|
|||
}
|
||||
case 'task': {
|
||||
const halfW = TASK_PILL.width / 2 + HIT_DETECTION.taskPadding;
|
||||
const halfH = TASK_PILL.height / 2 + HIT_DETECTION.taskPadding;
|
||||
const taskHeight = node.isOverflowStack ? KANBAN_ZONE.overflowHeight : TASK_PILL.height;
|
||||
const halfH = taskHeight / 2 + HIT_DETECTION.taskPadding;
|
||||
if (
|
||||
worldX >= x - halfW &&
|
||||
worldX <= x + halfW &&
|
||||
|
|
|
|||
|
|
@ -258,17 +258,22 @@ export const BACKGROUND = {
|
|||
|
||||
// ─── Kanban zone layout ─────────────────────────────────────────────────────
|
||||
|
||||
/** Max visible task rows per column. Includes the overflow stack row when present. */
|
||||
export const TASK_COLUMN_MAX_VISIBLE_ROWS = STABLE_SLOT_GEOMETRY.taskMaxVisibleRows;
|
||||
|
||||
export const KANBAN_ZONE = {
|
||||
/** Column width: task card (260) + gap (20) */
|
||||
columnWidth: 280,
|
||||
/** Row height: task card (72) + gap (8) */
|
||||
rowHeight: 80,
|
||||
/** Compact overflow footer height, matching activity/log more buttons */
|
||||
overflowHeight: 32,
|
||||
/** Task center offset from band top: header (20) + gap (4) + half card */
|
||||
headerHeight: 60,
|
||||
/** Zone starts this far below member node center */
|
||||
offsetY: 70,
|
||||
/** Column sequence: pending → wip → done → review → approved */
|
||||
columns: ['todo', 'wip', 'done', 'review', 'approved'] as const,
|
||||
/** Max tasks shown per column (overflow hidden) */
|
||||
maxVisibleRows: STABLE_SLOT_GEOMETRY.taskMaxVisibleRows,
|
||||
/** Max task rows shown per column. Includes the overflow stack row when present. */
|
||||
maxVisibleRows: TASK_COLUMN_MAX_VISIBLE_ROWS,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -7,31 +7,32 @@
|
|||
*/
|
||||
|
||||
// ─── Components ──────────────────────────────────────────────────────────────
|
||||
export { GraphView } from './ui/GraphView';
|
||||
export type { GraphViewProps } from './ui/GraphView';
|
||||
export { TASK_COLUMN_MAX_VISIBLE_ROWS } from './constants/canvas-constants';
|
||||
export { ACTIVITY_ANCHOR_LAYOUT, ACTIVITY_LANE } from './layout/activityLane';
|
||||
export { getTransientHandoffCardAlpha } from './ui/transientHandoffs';
|
||||
export type { TransientHandoffCard } from './ui/transientHandoffs';
|
||||
|
||||
// ─── Port Interfaces (for adapters in host project) ─────────────────────────
|
||||
export type { GraphConfigPort } from './ports/GraphConfigPort';
|
||||
export type { GraphDataPort } from './ports/GraphDataPort';
|
||||
export type { GraphEventPort } from './ports/GraphEventPort';
|
||||
export type { GraphConfigPort } from './ports/GraphConfigPort';
|
||||
|
||||
// ─── Port Types ──────────────────────────────────────────────────────────────
|
||||
export type {
|
||||
GraphNode,
|
||||
GraphEdge,
|
||||
GraphParticle,
|
||||
GraphActivityItem,
|
||||
GraphOwnerSlotAssignment,
|
||||
GraphLayoutPort,
|
||||
GraphDomainRef,
|
||||
GraphEdge,
|
||||
GraphEdgeType,
|
||||
GraphLaunchVisualState,
|
||||
GraphLayoutMode,
|
||||
GraphLayoutPort,
|
||||
GraphLayoutVersion,
|
||||
GraphNode,
|
||||
GraphNodeKind,
|
||||
GraphNodeState,
|
||||
GraphLaunchVisualState,
|
||||
GraphEdgeType,
|
||||
GraphOwnerSlotAssignment,
|
||||
GraphParticle,
|
||||
GraphParticleKind,
|
||||
GraphDomainRef,
|
||||
} from './ports/types';
|
||||
export type { GraphViewProps } from './ui/GraphView';
|
||||
export { GraphView } from './ui/GraphView';
|
||||
export type { TransientHandoffCard } from './ui/transientHandoffs';
|
||||
export { getTransientHandoffCardAlpha } from './ui/transientHandoffs';
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@
|
|||
* Class with ES #private methods, single source of truth from KANBAN_ZONE constants.
|
||||
*/
|
||||
|
||||
import type { GraphNode } from '../ports/types';
|
||||
import { KANBAN_ZONE, TASK_PILL } from '../constants/canvas-constants';
|
||||
import { COLORS } from '../constants/colors';
|
||||
|
||||
import type { GraphNode } from '../ports/types';
|
||||
import type { SlotFrame, StableRect } from './stableSlots';
|
||||
|
||||
/** Column header info for rendering */
|
||||
|
|
@ -18,10 +19,6 @@ export interface KanbanColumnHeader {
|
|||
x: number;
|
||||
y: number;
|
||||
color: string;
|
||||
/** Number of hidden overflow tasks in this column */
|
||||
overflowCount: number;
|
||||
/** Y position for the overflow badge */
|
||||
overflowY: number;
|
||||
}
|
||||
|
||||
/** Zone info per owner for rendering headers */
|
||||
|
|
@ -41,6 +38,18 @@ const COLUMN_LABELS: Record<string, { label: string; color: string }> = {
|
|||
approved: { label: 'Approved', color: COLORS.reviewApproved },
|
||||
};
|
||||
|
||||
function getOverflowFooterCenterY(baseY: number): number {
|
||||
const overflowGap = KANBAN_ZONE.rowHeight - TASK_PILL.height;
|
||||
return (
|
||||
baseY +
|
||||
KANBAN_ZONE.headerHeight +
|
||||
(KANBAN_ZONE.maxVisibleRows - 1) * KANBAN_ZONE.rowHeight +
|
||||
TASK_PILL.height / 2 +
|
||||
overflowGap +
|
||||
KANBAN_ZONE.overflowHeight / 2
|
||||
);
|
||||
}
|
||||
|
||||
export function getOwnerKanbanBaseX(args: {
|
||||
ownerX: number;
|
||||
ownerKind: GraphNode['kind'];
|
||||
|
|
@ -200,8 +209,6 @@ export class KanbanLayoutEngine {
|
|||
for (const [colIdx, col] of activeColumns.entries()) {
|
||||
const colX = baseX + colIdx * columnWidth;
|
||||
const config = COLUMN_LABELS[col.name] ?? { label: col.name, color: '#888' };
|
||||
const overflow = col.tasks.find((task) => task.isOverflowStack)?.overflowCount ?? 0;
|
||||
const visibleCount = col.tasks.length;
|
||||
|
||||
// Column header — centered over pill area (pill center = colX since drawTaskPill translates to x,y)
|
||||
headers.push({
|
||||
|
|
@ -209,14 +216,15 @@ export class KanbanLayoutEngine {
|
|||
x: colX, // pill center = task.x = colX
|
||||
y: baseY,
|
||||
color: config.color,
|
||||
overflowCount: overflow,
|
||||
overflowY: baseY + headerHeight + visibleCount * rowHeight,
|
||||
});
|
||||
|
||||
// Position tasks below header
|
||||
// Position tasks below header. Overflow stacks render as a compact footer after
|
||||
// the full visible task budget instead of consuming a task-card row.
|
||||
for (const [rowIdx, task] of col.tasks.entries()) {
|
||||
const targetX = colX;
|
||||
const targetY = baseY + headerHeight + rowIdx * rowHeight;
|
||||
const targetY = task.isOverflowStack
|
||||
? getOverflowFooterCenterY(baseY)
|
||||
: baseY + headerHeight + rowIdx * rowHeight;
|
||||
task.x = slotFrame
|
||||
? targetX
|
||||
: task.x != null
|
||||
|
|
@ -264,7 +272,6 @@ export class KanbanLayoutEngine {
|
|||
const baseX = unassignedTaskRect.left + TASK_PILL.width / 2;
|
||||
const headerY = unassignedTaskRect.top;
|
||||
const baseY = headerY + KANBAN_ZONE.headerHeight;
|
||||
const overflowCount = tasks.reduce((sum, task) => sum + (task.overflowCount ?? 0), 0);
|
||||
|
||||
this.zones.push({
|
||||
ownerId: '__unassigned__',
|
||||
|
|
@ -276,8 +283,6 @@ export class KanbanLayoutEngine {
|
|||
x: 0,
|
||||
y: headerY,
|
||||
color: COLORS.taskPending,
|
||||
overflowCount,
|
||||
overflowY: baseY + KANBAN_ZONE.maxVisibleRows * rowHeight,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -286,7 +291,7 @@ export class KanbanLayoutEngine {
|
|||
const col = idx % cols;
|
||||
const row = Math.floor(idx / cols);
|
||||
const targetX = baseX + col * columnWidth;
|
||||
const targetY = baseY + row * rowHeight;
|
||||
const targetY = task.isOverflowStack ? getOverflowFooterCenterY(headerY) : baseY + row * rowHeight;
|
||||
task.x = targetX;
|
||||
task.y = targetY;
|
||||
task.fx = targetX;
|
||||
|
|
@ -322,7 +327,6 @@ export class KanbanLayoutEngine {
|
|||
|
||||
// Add zone header for unassigned section
|
||||
if (tasks.length > 0) {
|
||||
const overflowCount = tasks.reduce((sum, task) => sum + (task.overflowCount ?? 0), 0);
|
||||
this.zones.push({
|
||||
ownerId: '__unassigned__',
|
||||
ownerX: centerX,
|
||||
|
|
@ -333,8 +337,6 @@ export class KanbanLayoutEngine {
|
|||
x: centerX,
|
||||
y: headerY,
|
||||
color: COLORS.taskPending,
|
||||
overflowCount,
|
||||
overflowY: baseY + KANBAN_ZONE.maxVisibleRows * rowHeight,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -344,7 +346,7 @@ export class KanbanLayoutEngine {
|
|||
const col = idx % cols;
|
||||
const row = Math.floor(idx / cols);
|
||||
const targetX = baseX + col * columnWidth;
|
||||
const targetY = baseY + row * rowHeight;
|
||||
const targetY = task.isOverflowStack ? getOverflowFooterCenterY(headerY) : baseY + row * rowHeight;
|
||||
task.x = task.x != null ? task.x + (targetX - task.x) * 0.15 : targetX;
|
||||
task.y = task.y != null ? task.y + (targetY - task.y) * 0.15 : targetY;
|
||||
task.fx = task.x;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { KANBAN_ZONE, TASK_PILL } from '../constants/canvas-constants';
|
||||
import type { GraphLayoutPort, GraphNode, GraphOwnerSlotAssignment } from '../ports/types';
|
||||
|
||||
import { ACTIVITY_LANE } from './activityLane';
|
||||
import type { WorldBounds } from './launchAnchor';
|
||||
import { STABLE_SLOT_GEOMETRY, STABLE_SLOT_SECTOR_VECTORS } from './stableSlotGeometry';
|
||||
|
||||
import type { GraphLayoutPort, GraphNode, GraphOwnerSlotAssignment } from '../ports/types';
|
||||
import type { WorldBounds } from './launchAnchor';
|
||||
|
||||
export type StableSlotWidthBucket = 'S' | 'M' | 'L';
|
||||
|
||||
export interface StableRect {
|
||||
|
|
@ -138,7 +140,11 @@ const SLOT_GEOMETRY = {
|
|||
boardColumnGap: 24,
|
||||
processRailMinWidth: STABLE_SLOT_GEOMETRY.processRailWidth,
|
||||
kanbanBandHeight:
|
||||
KANBAN_ZONE.headerHeight + STABLE_SLOT_GEOMETRY.taskMaxVisibleRows * KANBAN_ZONE.rowHeight,
|
||||
KANBAN_ZONE.headerHeight +
|
||||
(STABLE_SLOT_GEOMETRY.taskMaxVisibleRows - 1) * KANBAN_ZONE.rowHeight +
|
||||
TASK_PILL.height / 2 +
|
||||
(KANBAN_ZONE.rowHeight - TASK_PILL.height) +
|
||||
KANBAN_ZONE.overflowHeight,
|
||||
centralPadding: STABLE_SLOT_GEOMETRY.centralSafetyPadding,
|
||||
} as const;
|
||||
|
||||
|
|
@ -1455,7 +1461,7 @@ function buildRowOrbitSlotFrames(
|
|||
const rowTop = rowTopByIndex.get(row[0]!.rowIndex) ?? 0;
|
||||
const columnCount = rowCounts[row[0]!.rowIndex] ?? row.length;
|
||||
const columnWidths = resolveRowOrbitColumnWidths(row, columnCount, fallbackColumnWidth);
|
||||
let nextLeft = -getRowOrbitRowWidth(columnWidths) / 2;
|
||||
const nextLeft = -getRowOrbitRowWidth(columnWidths) / 2;
|
||||
for (const config of row) {
|
||||
const ownerX =
|
||||
nextLeft +
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
* Render strategy for task pill nodes.
|
||||
*/
|
||||
|
||||
import type { GraphNode } from '../ports/types';
|
||||
import type { NodeRenderStrategy, NodeRenderState } from './types';
|
||||
import { drawTasks } from '../canvas/draw-tasks';
|
||||
import { TASK_PILL, HIT_DETECTION } from '../constants/canvas-constants';
|
||||
import { HIT_DETECTION, KANBAN_ZONE, TASK_PILL } from '../constants/canvas-constants';
|
||||
|
||||
import type { GraphNode } from '../ports/types';
|
||||
import type { NodeRenderState, NodeRenderStrategy } from './types';
|
||||
|
||||
export class TaskStrategy implements NodeRenderStrategy {
|
||||
readonly kind = 'task' as const;
|
||||
|
|
@ -24,7 +25,8 @@ export class TaskStrategy implements NodeRenderStrategy {
|
|||
const x = node.x ?? 0;
|
||||
const y = node.y ?? 0;
|
||||
const halfW = TASK_PILL.width / 2 + HIT_DETECTION.taskPadding;
|
||||
const halfH = TASK_PILL.height / 2 + HIT_DETECTION.taskPadding;
|
||||
const taskHeight = node.isOverflowStack ? KANBAN_ZONE.overflowHeight : TASK_PILL.height;
|
||||
const halfH = taskHeight / 2 + HIT_DETECTION.taskPadding;
|
||||
return wx >= x - halfW && wx <= x + halfW && wy >= y - halfH && wy <= y + halfH;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"version": "0.0.39",
|
||||
"sourceRef": "v0.0.39",
|
||||
"version": "0.0.44",
|
||||
"sourceRef": "v0.0.44",
|
||||
"sourceRepository": "777genius/agent_teams_orchestrator",
|
||||
"releaseRepository": "777genius/agent-teams-ai",
|
||||
"releaseTag": "v2.0.0",
|
||||
"assets": {
|
||||
"darwin-arm64": {
|
||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.39.tar.gz",
|
||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.44.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"darwin-x64": {
|
||||
"file": "agent-teams-runtime-darwin-x64-v0.0.39.tar.gz",
|
||||
"file": "agent-teams-runtime-darwin-x64-v0.0.44.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"linux-x64": {
|
||||
"file": "agent-teams-runtime-linux-x64-v0.0.39.tar.gz",
|
||||
"file": "agent-teams-runtime-linux-x64-v0.0.44.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"win32-x64": {
|
||||
"file": "agent-teams-runtime-win32-x64-v0.0.39.zip",
|
||||
"file": "agent-teams-runtime-win32-x64-v0.0.44.zip",
|
||||
"archiveKind": "zip",
|
||||
"binaryName": "claude-multimodel.exe"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ const PLATFORM_ARGS = {
|
|||
};
|
||||
|
||||
const LINUX_PACKAGE_NAME_OVERRIDES = [
|
||||
'--config.productName=Agent-Teams-UI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams UI',
|
||||
'--config.productName=Agent-Teams-AI',
|
||||
'--config.linux.desktop.entry.Name=Agent Teams AI',
|
||||
];
|
||||
|
||||
function buildElectronBuilderInvocations(argv) {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,12 @@ function findExecutable(bundlePath, platform) {
|
|||
const packageJson = fs.existsSync(packageJsonPath)
|
||||
? JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||||
: {};
|
||||
const preferredNames = [packageJson.name, 'agent-teams-ai', 'Agent Teams UI'].filter(Boolean);
|
||||
const preferredNames = [
|
||||
packageJson.name,
|
||||
'agent-teams-ai',
|
||||
'Agent Teams AI',
|
||||
'Agent Teams UI',
|
||||
].filter(Boolean);
|
||||
for (const name of preferredNames) {
|
||||
const candidate = path.join(bundlePath, name);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
|
|
|
|||
37
scripts/lib/live-smoke-runtime.mjs
Normal file
37
scripts/lib/live-smoke-runtime.mjs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import path from 'node:path';
|
||||
|
||||
export function resolveLiveSmokeOrchestratorCliPath({
|
||||
env = process.env,
|
||||
repoRoot,
|
||||
} = {}) {
|
||||
const explicitCliPath = env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
||||
if (explicitCliPath) {
|
||||
return explicitCliPath;
|
||||
}
|
||||
|
||||
const configuredRuntimeRoot = env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const baseRepoRoot = repoRoot ? path.resolve(repoRoot) : process.cwd();
|
||||
const runtimeRoot = configuredRuntimeRoot
|
||||
? path.resolve(configuredRuntimeRoot)
|
||||
: path.resolve(baseRepoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
return path.join(runtimeRoot, 'cli-source');
|
||||
}
|
||||
|
||||
export function resolveReleaseSmokeOrchestratorCliPath({
|
||||
env = process.env,
|
||||
repoRoot,
|
||||
} = {}) {
|
||||
const explicitCliPath = env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim();
|
||||
if (explicitCliPath) {
|
||||
return explicitCliPath;
|
||||
}
|
||||
|
||||
const configuredRuntimeRoot = env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const baseRepoRoot = repoRoot ? path.resolve(repoRoot) : process.cwd();
|
||||
const runtimeRoot = configuredRuntimeRoot
|
||||
? path.resolve(configuredRuntimeRoot)
|
||||
: path.resolve(baseRepoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
return path.join(runtimeRoot, 'cli');
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import path from 'node:path';
|
|||
const CHILD_CLOSE_GRACE_MS = 3_000;
|
||||
const CHILD_FORCE_CLOSE_GRACE_MS = 1_000;
|
||||
const TASKKILL_TIMEOUT_MS = 5_000;
|
||||
const OPENCODE_HEALTH_FETCH_TIMEOUT_MS = 1_000;
|
||||
|
||||
export async function preflightOpenCodeLiveEnvironment(input) {
|
||||
const repoRoot = input.repoRoot;
|
||||
|
|
@ -125,13 +126,12 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
|
|||
return { ok: false, reason: output || `process exited with code ${child.exitCode}` };
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/global/health`);
|
||||
if (response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (data?.healthy === true) {
|
||||
return { ok: true };
|
||||
}
|
||||
const response = await fetchOpenCodeHealth(port);
|
||||
if (isHealthyOpenCodeHostResponse(response)) {
|
||||
response.body?.cancel().catch(() => undefined);
|
||||
return { ok: true };
|
||||
}
|
||||
response.body?.cancel().catch(() => undefined);
|
||||
} catch {
|
||||
// Host is still starting.
|
||||
}
|
||||
|
|
@ -143,6 +143,22 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchOpenCodeHealth(port) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), OPENCODE_HEALTH_FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(`http://127.0.0.1:${port}/global/health`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function isHealthyOpenCodeHostResponse(response) {
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
async function stopChild(child, options = {}) {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const killProcessTree = options.killProcessTree ?? taskkillProcessTree;
|
||||
|
|
@ -271,6 +287,7 @@ function compactOutput(value) {
|
|||
}
|
||||
|
||||
export const __opencodeLivePreflightTestHooks = {
|
||||
isHealthyOpenCodeHostResponse,
|
||||
stopChild,
|
||||
taskkillProcessTree,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,18 +5,24 @@ import path from 'node:path';
|
|||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
AGENT_CLI_LAUNCH_LIVE_E2E: '1',
|
||||
CLAUDE_TEAM_CLI_FLAVOR: process.env.CLAUDE_TEAM_CLI_FLAVOR || 'agent_teams_orchestrator',
|
||||
CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH:
|
||||
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH || path.join(siblingOrchestrator, 'cli'),
|
||||
};
|
||||
|
||||
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = resolveLiveSmokeOrchestratorCliPath({
|
||||
env,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Running agent CLI launch live smoke');
|
||||
console.log(`Claude runtime: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from 'node:path';
|
|||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
|
||||
import {
|
||||
exitForSkippedPreflight,
|
||||
preflightOpenCodeLiveEnvironment,
|
||||
|
|
@ -12,8 +13,6 @@ import {
|
|||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
|
@ -26,8 +25,10 @@ const env = {
|
|||
};
|
||||
|
||||
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = resolveLiveSmokeOrchestratorCliPath({
|
||||
env,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Running OpenCode mixed recovery live smoke');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from 'node:path';
|
|||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
|
||||
import {
|
||||
exitForSkippedPreflight,
|
||||
preflightOpenCodeLiveEnvironment,
|
||||
|
|
@ -12,8 +13,6 @@ import {
|
|||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
|
@ -33,8 +32,10 @@ const env = {
|
|||
};
|
||||
|
||||
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = resolveLiveSmokeOrchestratorCliPath({
|
||||
env,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Running OpenCode semantic gauntlet live smoke');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from 'node:path';
|
|||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
|
||||
import {
|
||||
exitForSkippedPreflight,
|
||||
preflightOpenCodeLiveEnvironment,
|
||||
|
|
@ -12,8 +13,6 @@ import {
|
|||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
|
@ -25,8 +24,10 @@ const env = {
|
|||
};
|
||||
|
||||
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = resolveLiveSmokeOrchestratorCliPath({
|
||||
env,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Running OpenCode semantic messaging live smoke');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from 'node:path';
|
|||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
|
||||
import {
|
||||
exitForSkippedPreflight,
|
||||
preflightOpenCodeLiveEnvironment,
|
||||
|
|
@ -12,8 +13,6 @@ import {
|
|||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
|
@ -24,8 +23,10 @@ const env = {
|
|||
};
|
||||
|
||||
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = resolveLiveSmokeOrchestratorCliPath({
|
||||
env,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Running OpenCode semantic model matrix live smoke');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import path from 'node:path';
|
|||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
|
||||
import {
|
||||
exitForSkippedPreflight,
|
||||
preflightOpenCodeLiveEnvironment,
|
||||
|
|
@ -12,8 +13,6 @@ import {
|
|||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
|
|
@ -25,8 +24,10 @@ const env = {
|
|||
};
|
||||
|
||||
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = resolveLiveSmokeOrchestratorCliPath({
|
||||
env,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Running OpenCode team provisioning live smoke');
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ import path from 'node:path';
|
|||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
|
||||
import { preflightOpenCodeLiveEnvironment } from './lib/opencode-live-preflight.mjs';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, '..');
|
||||
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||
const requestedOrder =
|
||||
process.env.PROVIDER_LAUNCH_STRESS_ORDER?.trim() || 'anthropic,codex,opencode,mixed';
|
||||
|
||||
|
|
@ -25,14 +24,20 @@ const env = {
|
|||
PROVIDER_LAUNCH_STRESS_ANTHROPIC_AUTH:
|
||||
process.env.PROVIDER_LAUNCH_STRESS_ANTHROPIC_AUTH?.trim() ||
|
||||
(process.env.ANTHROPIC_API_KEY?.trim() ? 'api-key' : 'subscription'),
|
||||
CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS:
|
||||
process.env.CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS?.trim() || '90000',
|
||||
CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS:
|
||||
process.env.CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS?.trim() || '30000',
|
||||
OPENCODE_E2E: '1',
|
||||
OPENCODE_E2E_USE_REAL_APP_CREDENTIALS: '1',
|
||||
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
|
||||
};
|
||||
|
||||
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = resolveLiveSmokeOrchestratorCliPath({
|
||||
env,
|
||||
repoRoot,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Running provider launch stress live smoke');
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ export function collapseOverflowStacksWithMeta(
|
|||
continue;
|
||||
}
|
||||
|
||||
const keptTasks = groupTasks.slice(0, maxVisibleRows - 1);
|
||||
const hiddenTasks = groupTasks.slice(maxVisibleRows - 1);
|
||||
const keptTasks = groupTasks.slice(0, maxVisibleRows);
|
||||
const hiddenTasks = groupTasks.slice(maxVisibleRows);
|
||||
const representative = hiddenTasks[0] ?? groupTasks[groupTasks.length - 1];
|
||||
const columnKey = resolveOverflowColumnKey(representative);
|
||||
const ownerMemberName = extractOwnerMemberName(representative, teamName);
|
||||
|
|
@ -96,10 +96,10 @@ export function collapseOverflowStacksWithMeta(
|
|||
visibleTasks.push({
|
||||
id: `task:${teamName}:overflow:${groupKey}`,
|
||||
kind: 'task',
|
||||
label: `+${hiddenTasks.length}`,
|
||||
label: `+${hiddenTasks.length} more`,
|
||||
state: representative.state,
|
||||
displayId: `+${hiddenTasks.length}`,
|
||||
sublabel: `${hiddenTasks.length} more tasks`,
|
||||
displayId: `+${hiddenTasks.length} more`,
|
||||
sublabel: undefined,
|
||||
ownerId: representative.ownerId ?? null,
|
||||
taskStatus: representative.taskStatus,
|
||||
reviewState: representative.reviewState,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,17 @@
|
|||
* Class-based with ES #private fields and DI-ready constructor.
|
||||
*/
|
||||
|
||||
import {
|
||||
type GraphDataPort,
|
||||
type GraphEdge,
|
||||
type GraphLayoutMode,
|
||||
type GraphLayoutPort,
|
||||
type GraphNode,
|
||||
type GraphNodeState,
|
||||
type GraphOwnerSlotAssignment,
|
||||
type GraphParticle,
|
||||
TASK_COLUMN_MAX_VISIBLE_ROWS,
|
||||
} from '@claude-teams/agent-graph';
|
||||
import { getUnreadCount } from '@renderer/services/commentReadStorage';
|
||||
import {
|
||||
agentAvatarUrl,
|
||||
|
|
@ -50,32 +61,24 @@ import {
|
|||
resolveTaskReviewer,
|
||||
} from '../../core/domain/taskGraphSemantics';
|
||||
|
||||
import type {
|
||||
GraphDataPort,
|
||||
GraphEdge,
|
||||
GraphLayoutMode,
|
||||
GraphLayoutPort,
|
||||
GraphNode,
|
||||
GraphNodeState,
|
||||
GraphOwnerSlotAssignment,
|
||||
GraphParticle,
|
||||
} from '@claude-teams/agent-graph';
|
||||
import type {
|
||||
ActiveToolCall,
|
||||
InboxMessage,
|
||||
LeadActivityState,
|
||||
LeadContextUsage,
|
||||
MemberSpawnStatusEntry,
|
||||
MemberSpawnStatusesSnapshot,
|
||||
ResolvedTeamMember,
|
||||
TeamAgentRuntimeEntry,
|
||||
TeamProcess,
|
||||
TeamProvisioningProgress,
|
||||
TeamViewSnapshot,
|
||||
} from '@shared/types/team';
|
||||
import type { LeadContextUsage } from '@shared/types/team';
|
||||
|
||||
export interface TeamGraphData extends TeamViewSnapshot {
|
||||
members: ResolvedTeamMember[];
|
||||
messageFeed: InboxMessage[];
|
||||
runtimeEntriesByMember?: Record<string, TeamAgentRuntimeEntry>;
|
||||
}
|
||||
|
||||
function toGraphLaunchVisualState(
|
||||
|
|
@ -438,6 +441,7 @@ export class TeamGraphAdapter {
|
|||
): void {
|
||||
const percent = leadContext?.contextUsedPercent;
|
||||
const leadMember = data.members.find((member) => member.name === leadName);
|
||||
const runtimeEntry = data.runtimeEntriesByMember?.[leadName];
|
||||
const isTeamVisualOnline = data.isAlive || isTeamProvisioning;
|
||||
const activeTool = TeamGraphAdapter.#selectVisibleTool(
|
||||
activeTools?.[leadName],
|
||||
|
|
@ -454,6 +458,7 @@ export class TeamGraphAdapter {
|
|||
spawnLivenessSource: undefined,
|
||||
spawnRuntimeAlive: undefined,
|
||||
spawnBootstrapStalled: undefined,
|
||||
runtimeEntry,
|
||||
runtimeAdvisory: leadMember.runtimeAdvisory,
|
||||
isLaunchSettling: false,
|
||||
isTeamAlive: data.isAlive,
|
||||
|
|
@ -545,6 +550,7 @@ export class TeamGraphAdapter {
|
|||
const memberId =
|
||||
memberNodeIdByAlias.get(member.name) ?? buildGraphMemberNodeIdForMember(teamName, member);
|
||||
const spawn = spawnStatuses?.[member.name];
|
||||
const runtimeEntry = data.runtimeEntriesByMember?.[member.name];
|
||||
const activeTool = TeamGraphAdapter.#selectVisibleTool(
|
||||
activeTools?.[member.name],
|
||||
finishedVisible?.[member.name]
|
||||
|
|
@ -576,6 +582,9 @@ export class TeamGraphAdapter {
|
|||
spawnAgentToolAccepted: spawn?.agentToolAccepted,
|
||||
spawnHardFailure: spawn?.hardFailure,
|
||||
spawnLivenessKind: spawn?.livenessKind,
|
||||
spawnFirstSpawnAcceptedAt: spawn?.firstSpawnAcceptedAt,
|
||||
spawnUpdatedAt: spawn?.updatedAt,
|
||||
runtimeEntry,
|
||||
runtimeAdvisory: member.runtimeAdvisory,
|
||||
isLaunchSettling,
|
||||
isTeamAlive: data.isAlive,
|
||||
|
|
@ -751,7 +760,7 @@ export class TeamGraphAdapter {
|
|||
}
|
||||
|
||||
const { visibleNodes: visibleTaskNodes, visibleNodeIdByTaskId } =
|
||||
collapseOverflowStacksWithMeta(rawTaskNodes, teamName, 5);
|
||||
collapseOverflowStacksWithMeta(rawTaskNodes, teamName, TASK_COLUMN_MAX_VISIBLE_ROWS);
|
||||
const visibleTaskIds = new Set(
|
||||
visibleTaskNodes.flatMap((taskNode) =>
|
||||
taskNode.domainRef.kind === 'task' ? [taskNode.domainRef.taskId] : []
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface GraphMemberPopoverContext {
|
|||
| null;
|
||||
teamMembers: ReturnType<typeof selectResolvedMembersForTeamName>;
|
||||
spawnEntry: AppState['memberSpawnStatusesByTeam'][string][string] | undefined;
|
||||
runtimeEntry: AppState['teamAgentRuntimeByTeam'][string]['members'][string] | undefined;
|
||||
leadActivity: AppState['leadActivityByTeam'][string] | undefined;
|
||||
progress: ReturnType<typeof getCurrentProvisioningProgressForTeam> | null;
|
||||
memberSpawnSnapshot: AppState['memberSpawnSnapshotsByTeam'][string] | undefined;
|
||||
|
|
@ -41,6 +42,9 @@ function selectGraphMemberPopoverContext(
|
|||
: null,
|
||||
teamMembers,
|
||||
spawnEntry: teamName ? state.memberSpawnStatusesByTeam[teamName]?.[memberName] : undefined,
|
||||
runtimeEntry: teamName
|
||||
? state.teamAgentRuntimeByTeam[teamName]?.members[memberName]
|
||||
: undefined,
|
||||
leadActivity: teamName ? state.leadActivityByTeam[teamName] : undefined,
|
||||
progress: teamName ? getCurrentProvisioningProgressForTeam(state, teamName) : null,
|
||||
memberSpawnSnapshot: teamName ? state.memberSpawnSnapshotsByTeam[teamName] : undefined,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
||||
|
||||
import { useTeamAgentRuntimeWatcher } from '@renderer/components/team/useTeamAgentRuntimeWatcher';
|
||||
import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
|
||||
import { useStore } from '@renderer/store';
|
||||
import {
|
||||
|
|
@ -73,6 +74,7 @@ export function useTeamGraphAdapter(
|
|||
members,
|
||||
messages,
|
||||
spawnStatuses,
|
||||
runtimeSnapshot,
|
||||
leadActivity,
|
||||
leadContext,
|
||||
pendingApprovals,
|
||||
|
|
@ -93,6 +95,7 @@ export function useTeamGraphAdapter(
|
|||
members: isActive ? selectResolvedMembersForTeamName(s, teamName) : EMPTY_MEMBERS,
|
||||
messages: isActive ? selectTeamMessages(s, teamName) : EMPTY_MESSAGES,
|
||||
spawnStatuses: isActive && teamName ? s.memberSpawnStatusesByTeam[teamName] : undefined,
|
||||
runtimeSnapshot: isActive && teamName ? s.teamAgentRuntimeByTeam[teamName] : undefined,
|
||||
leadActivity: isActive && teamName ? s.leadActivityByTeam[teamName] : undefined,
|
||||
leadContext: isActive && teamName ? s.leadContextByTeam[teamName] : undefined,
|
||||
pendingApprovals: isActive ? s.pendingApprovals : EMPTY_PENDING_APPROVALS,
|
||||
|
|
@ -113,6 +116,11 @@ export function useTeamGraphAdapter(
|
|||
}))
|
||||
);
|
||||
|
||||
useTeamAgentRuntimeWatcher({
|
||||
teamName,
|
||||
enabled: isActive,
|
||||
});
|
||||
|
||||
const pendingApprovalAgents = useMemo(() => {
|
||||
if (!isActive) {
|
||||
return EMPTY_PENDING_APPROVAL_AGENTS;
|
||||
|
|
@ -134,8 +142,9 @@ export function useTeamGraphAdapter(
|
|||
...teamSnapshot,
|
||||
members,
|
||||
messageFeed: messages,
|
||||
runtimeEntriesByMember: runtimeSnapshot?.members,
|
||||
};
|
||||
}, [members, messages, teamSnapshot]);
|
||||
}, [members, messages, runtimeSnapshot?.members, teamSnapshot]);
|
||||
|
||||
const commentReadState = useSyncExternalStore(
|
||||
isActive ? subscribe : subscribeNoop,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const ACTIVITY_SHELL_HEIGHT =
|
|||
ACTIVITY_LANE.maxVisibleItems * ACTIVITY_LANE.rowHeight +
|
||||
ACTIVITY_LANE.overflowHeight;
|
||||
const NEW_ACTIVITY_HIGHLIGHT_MS = 1_000;
|
||||
const INTERACTIVE_ACTIVITY_CONTROL_CLASS = 'pointer-events-auto';
|
||||
|
||||
interface GraphActivityHudProps {
|
||||
teamName: string;
|
||||
|
|
@ -456,7 +457,7 @@ export const GraphActivityHud = ({
|
|||
key={entry.graphItem.id}
|
||||
data-activity-entry-id={entry.graphItem.id}
|
||||
className={[
|
||||
'h-[72px] min-h-[72px] min-w-0 max-w-full cursor-pointer overflow-hidden rounded-md border transition-[border-color,background-color,box-shadow] duration-500',
|
||||
`${INTERACTIVE_ACTIVITY_CONTROL_CLASS} h-[72px] min-h-[72px] min-w-0 max-w-full cursor-pointer overflow-hidden rounded-md border transition-[border-color,background-color,box-shadow] duration-500`,
|
||||
isHighlighted
|
||||
? 'border-sky-300/70 bg-[rgba(14,34,62,0.56)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)]'
|
||||
: 'border-transparent',
|
||||
|
|
@ -536,7 +537,7 @@ export const GraphActivityHud = ({
|
|||
ref={(element) => {
|
||||
shellRefs.current.set(lane.node.id, element);
|
||||
}}
|
||||
className="pointer-events-auto absolute z-10 origin-top-left select-none opacity-0"
|
||||
className="pointer-events-none absolute z-10 origin-top-left select-none opacity-0"
|
||||
style={{
|
||||
width: `${laneWidth}px`,
|
||||
maxWidth: `${laneWidth}px`,
|
||||
|
|
@ -561,7 +562,7 @@ export const GraphActivityHud = ({
|
|||
{lane.overflowCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 min-h-8 w-full rounded-md border border-white/10 bg-[rgba(8,14,28,0.64)] px-3 py-1 text-center text-[11px] font-medium text-slate-300 transition-colors hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]"
|
||||
className={`${INTERACTIVE_ACTIVITY_CONTROL_CLASS} h-8 min-h-8 w-full rounded-md border border-white/10 bg-[rgba(8,14,28,0.64)] px-3 py-1 text-center text-[11px] font-medium text-slate-300 transition-colors hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]`}
|
||||
onClick={() => handleOpenOwnerActivity(lane.node)}
|
||||
>
|
||||
+{lane.overflowCount} more
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ export const GraphMemberLogPreviewHud = ({
|
|||
}}
|
||||
>
|
||||
<div className="flex h-full min-w-0 max-w-full flex-col overflow-hidden">
|
||||
<div className="flex h-4 min-h-4 items-center gap-1 px-1 text-[9px] font-semibold tracking-[0.18em] text-slate-400/70">
|
||||
<div className="mb-1 flex h-4 min-h-4 items-center gap-1 px-1 text-[9px] font-semibold tracking-[0.18em] text-slate-400/70">
|
||||
<Wrench className="size-2.5 text-slate-500" />
|
||||
Logs
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ const MemberPopoverContent = ({
|
|||
teamData,
|
||||
teamMembers,
|
||||
spawnEntry,
|
||||
runtimeEntry,
|
||||
leadActivity,
|
||||
progress,
|
||||
memberSpawnSnapshot,
|
||||
|
|
@ -358,6 +359,9 @@ const MemberPopoverContent = ({
|
|||
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
||||
spawnHardFailure: spawnEntry?.hardFailure,
|
||||
spawnLivenessKind: spawnEntry?.livenessKind,
|
||||
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
||||
spawnUpdatedAt: spawnEntry?.updatedAt,
|
||||
runtimeEntry,
|
||||
runtimeAdvisory: member.runtimeAdvisory,
|
||||
isLaunchSettling: provisioningPresentation?.hasMembersStillJoining ?? false,
|
||||
isTeamAlive: teamData?.isAlive,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ export interface AnthropicRuntimeReconciliation {
|
|||
fastModeResetReason: string | null;
|
||||
}
|
||||
|
||||
export type AnthropicEffortSupportResolution =
|
||||
| { kind: 'supported'; source: 'catalog' | 'runtime-capability' | 'static-fallback' }
|
||||
| { kind: 'unsupported-by-catalog'; supportedEfforts: EffortLevel[] }
|
||||
| { kind: 'unsupported-by-runtime-capability'; supportedEfforts: EffortLevel[] }
|
||||
| { kind: 'unverified-catalog-missing' };
|
||||
|
||||
function getAnthropicCatalog(
|
||||
source: AnthropicRuntimeProfileSource
|
||||
): CliProviderModelCatalog | null {
|
||||
|
|
@ -77,17 +83,89 @@ function hasCatalogTruth(selection: AnthropicRuntimeSelection): boolean {
|
|||
return selection.catalogSource !== 'unavailable' && selection.catalogStatus !== 'unavailable';
|
||||
}
|
||||
|
||||
function stripOneMillionSuffix(model: string): string {
|
||||
return model
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/(?:\[1m\])+$/i, '');
|
||||
}
|
||||
|
||||
function isKnownAnthropicReasoningModel(model: string | null | undefined): boolean {
|
||||
const normalized = stripOneMillionSuffix(model ?? '');
|
||||
return (
|
||||
normalized === 'opus' ||
|
||||
normalized === 'sonnet' ||
|
||||
normalized === 'claude-opus-4-7' ||
|
||||
normalized.startsWith('claude-opus-4-7-') ||
|
||||
normalized === 'claude-opus-4-6' ||
|
||||
normalized.startsWith('claude-opus-4-6-') ||
|
||||
normalized === 'claude-sonnet-4-6' ||
|
||||
normalized.startsWith('claude-sonnet-4-6-')
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRuntimeReasoningEfforts(
|
||||
capabilities: CliProviderRuntimeCapabilities | null | undefined
|
||||
): EffortLevel[] {
|
||||
return normalizeEffortLevels(capabilities?.reasoningEffort?.values);
|
||||
}
|
||||
|
||||
export function resolveAnthropicEffortSupport(params: {
|
||||
selection: AnthropicRuntimeSelection;
|
||||
effort: EffortLevel;
|
||||
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
|
||||
}): AnthropicEffortSupportResolution {
|
||||
if (params.selection.catalogModel) {
|
||||
return params.selection.supportedEfforts.includes(params.effort)
|
||||
? { kind: 'supported', source: 'catalog' }
|
||||
: { kind: 'unsupported-by-catalog', supportedEfforts: params.selection.supportedEfforts };
|
||||
}
|
||||
|
||||
const runtimeReasoning = params.runtimeCapabilities?.reasoningEffort;
|
||||
const runtimeEfforts = normalizeRuntimeReasoningEfforts(params.runtimeCapabilities);
|
||||
if (runtimeReasoning) {
|
||||
if (
|
||||
runtimeReasoning.supported !== true ||
|
||||
runtimeReasoning.configPassthrough !== true ||
|
||||
!runtimeEfforts.includes(params.effort)
|
||||
) {
|
||||
return { kind: 'unsupported-by-runtime-capability', supportedEfforts: runtimeEfforts };
|
||||
}
|
||||
if (isKnownAnthropicReasoningModel(params.selection.resolvedLaunchModel)) {
|
||||
return { kind: 'supported', source: 'runtime-capability' };
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!runtimeReasoning &&
|
||||
isKnownAnthropicReasoningModel(params.selection.resolvedLaunchModel) &&
|
||||
(params.effort === 'low' ||
|
||||
params.effort === 'medium' ||
|
||||
params.effort === 'high' ||
|
||||
params.effort === 'max')
|
||||
) {
|
||||
return { kind: 'supported', source: 'static-fallback' };
|
||||
}
|
||||
|
||||
return { kind: 'unverified-catalog-missing' };
|
||||
}
|
||||
|
||||
export function resolveAnthropicRuntimeSelection(params: {
|
||||
source: AnthropicRuntimeProfileSource;
|
||||
selectedModel?: string | null;
|
||||
limitContext: boolean;
|
||||
availableLaunchModels?: Iterable<string>;
|
||||
}): AnthropicRuntimeSelection {
|
||||
const catalog = getAnthropicCatalog(params.source);
|
||||
const availableLaunchModels =
|
||||
catalog !== null
|
||||
? catalog.models.map((model) => model.launchModel)
|
||||
: params.availableLaunchModels;
|
||||
const resolvedLaunchModel =
|
||||
resolveAnthropicLaunchModel({
|
||||
selectedModel: params.selectedModel,
|
||||
limitContext: params.limitContext,
|
||||
availableLaunchModels: catalog?.models.map((model) => model.launchModel),
|
||||
availableLaunchModels,
|
||||
defaultLaunchModel: catalog?.defaultLaunchModel ?? null,
|
||||
}) ?? null;
|
||||
|
||||
|
|
@ -168,6 +246,7 @@ export function reconcileAnthropicRuntimeSelections(params: {
|
|||
selectedEffort?: string | null;
|
||||
selectedFastMode?: TeamFastMode | null;
|
||||
providerFastModeDefault?: boolean;
|
||||
runtimeCapabilities?: CliProviderRuntimeCapabilities | null;
|
||||
}): AnthropicRuntimeReconciliation {
|
||||
const selectedEffort = normalizeEffortLevel(params.selectedEffort ?? null);
|
||||
if (!hasCatalogTruth(params.selection)) {
|
||||
|
|
@ -179,14 +258,22 @@ export function reconcileAnthropicRuntimeSelections(params: {
|
|||
};
|
||||
}
|
||||
|
||||
const nextEffort =
|
||||
selectedEffort && !params.selection.supportedEfforts.includes(selectedEffort)
|
||||
? ''
|
||||
: (selectedEffort ?? '');
|
||||
const effortResetReason =
|
||||
selectedEffort && nextEffort === ''
|
||||
? `${selectedEffort} effort is not available for the currently selected Anthropic model. Reset to Default.`
|
||||
: null;
|
||||
let nextEffort: EffortLevel | '' = selectedEffort ?? '';
|
||||
let effortResetReason: string | null = null;
|
||||
if (selectedEffort) {
|
||||
const effortSupport = resolveAnthropicEffortSupport({
|
||||
selection: params.selection,
|
||||
effort: selectedEffort,
|
||||
runtimeCapabilities: params.runtimeCapabilities,
|
||||
});
|
||||
if (
|
||||
effortSupport.kind === 'unsupported-by-catalog' ||
|
||||
effortSupport.kind === 'unsupported-by-runtime-capability'
|
||||
) {
|
||||
nextEffort = '';
|
||||
effortResetReason = `${selectedEffort} effort is not available for the currently selected Anthropic model. Reset to Default.`;
|
||||
}
|
||||
}
|
||||
|
||||
const fastResolution = resolveAnthropicFastMode({
|
||||
selection: params.selection,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export type {
|
||||
AnthropicEffortSupportResolution,
|
||||
AnthropicFastModeResolution,
|
||||
AnthropicRuntimeProfileSource,
|
||||
AnthropicRuntimeReconciliation,
|
||||
|
|
@ -6,6 +7,7 @@ export type {
|
|||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
export {
|
||||
reconcileAnthropicRuntimeSelections,
|
||||
resolveAnthropicEffortSupport,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export type {
|
||||
AnthropicEffortSupportResolution,
|
||||
AnthropicFastModeResolution,
|
||||
AnthropicRuntimeProfileSource,
|
||||
AnthropicRuntimeReconciliation,
|
||||
|
|
@ -6,6 +7,7 @@ export type {
|
|||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
export {
|
||||
reconcileAnthropicRuntimeSelections,
|
||||
resolveAnthropicEffortSupport,
|
||||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '../core/domain/resolveAnthropicRuntimeProfile';
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ export class CodexAppServerClient {
|
|||
{
|
||||
clientInfo: {
|
||||
name: 'agent-teams-ai',
|
||||
title: 'Agent Teams UI',
|
||||
title: 'Agent Teams AI',
|
||||
version: '0.1.0',
|
||||
},
|
||||
capabilities: {
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ describe('planTeamRuntimeLanes', () => {
|
|||
ok: false,
|
||||
reason: 'unsupported_opencode_led_mixed_team',
|
||||
message:
|
||||
'Mixed teams with an OpenCode lead are not supported in this phase. Keep the team lead on Anthropic, Codex, or Gemini when you mix OpenCode with other providers.',
|
||||
'Mixed teams with an OpenCode lead are not supported in this phase. Keep the team lead on Anthropic or Codex when you mix OpenCode with other providers.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export function planTeamRuntimeLanes(params: {
|
|||
ok: false,
|
||||
reason: 'unsupported_opencode_led_mixed_team',
|
||||
message:
|
||||
'Mixed teams with an OpenCode lead are not supported in this phase. Keep the team lead on Anthropic, Codex, or Gemini when you mix OpenCode with other providers.',
|
||||
'Mixed teams with an OpenCode lead are not supported in this phase. Keep the team lead on Anthropic or Codex when you mix OpenCode with other providers.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export function buildTmuxEffectiveAvailability(
|
|||
version: input.host.version,
|
||||
binaryPath: input.host.binaryPath,
|
||||
runtimeReady: input.nativeSupported,
|
||||
detail: 'tmux is available as an optional pane transport for teammate sessions.',
|
||||
detail: 'tmux is available.',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ export class TmuxInstallStrategyResolver {
|
|||
if (input.effective.available) {
|
||||
return input.effective.location === 'wsl'
|
||||
? 'tmux is available inside WSL on Windows.'
|
||||
: 'tmux is available as an optional pane transport for teammate sessions.';
|
||||
: 'tmux is available.';
|
||||
}
|
||||
|
||||
if (input.platform === 'darwin') {
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ describe('TmuxInstallerBannerAdapter', () => {
|
|||
version: 'tmux 3.6a',
|
||||
binaryPath: '/opt/homebrew/bin/tmux',
|
||||
runtimeReady: true,
|
||||
detail: 'tmux is available as an optional pane transport.',
|
||||
detail: 'tmux is available.',
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Main process entry point for Agent Teams UI.
|
||||
* Main process entry point for Agent Teams AI.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Initialize Electron app and main window
|
||||
|
|
@ -70,6 +70,7 @@ import { ChangeExtractorService } from '@main/services/team/ChangeExtractorServi
|
|||
import { CrossTeamService } from '@main/services/team/CrossTeamService';
|
||||
import { FileContentResolver } from '@main/services/team/FileContentResolver';
|
||||
import { GitDiffFallback } from '@main/services/team/GitDiffFallback';
|
||||
import { isInformationalOpenCodeRuntimeDeliveryDiagnostic } from '@main/services/team/opencode/delivery/OpenCodeRuntimeDeliveryDiagnostics';
|
||||
import {
|
||||
copyOpenCodeLocalMcpLaunchEnv,
|
||||
hasOpenCodeLocalMcpLaunchEnv,
|
||||
|
|
@ -237,6 +238,13 @@ const logger = createLogger('App');
|
|||
const appStartedAtMs = Date.now();
|
||||
const openCodeManagedHostInstanceId = `${process.pid}-${appStartedAtMs}`;
|
||||
let openCodeLifecycleBridge: OpenCodeReadinessBridge | null = null;
|
||||
|
||||
function hasWarningRelayDiagnostics(diagnostics: readonly string[]): boolean {
|
||||
return diagnostics.some(
|
||||
(diagnostic) => !isInformationalOpenCodeRuntimeDeliveryDiagnostic(diagnostic)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
earlyElectronUserDataMigrationResult.migrated &&
|
||||
earlyElectronUserDataMigrationResult.legacyPath &&
|
||||
|
|
@ -1268,9 +1276,12 @@ function wireFileWatcherEvents(context: ServiceContext): void {
|
|||
.relayInboxFileToLiveRecipient(teamName, inboxName)
|
||||
.then((relay) => {
|
||||
if (relay.diagnostics?.length) {
|
||||
logger.warn(
|
||||
`[FileWatcher] relay diagnostics for ${teamName}/${inboxName}: ${relay.diagnostics.join('; ')}`
|
||||
);
|
||||
const message = `[FileWatcher] relay diagnostics for ${teamName}/${inboxName}: ${relay.diagnostics.join('; ')}`;
|
||||
if (hasWarningRelayDiagnostics(relay.diagnostics)) {
|
||||
logger.warn(message);
|
||||
} else {
|
||||
logger.info(message);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e: unknown) =>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,20 @@ const editorFileWatcher = new EditorFileWatcher();
|
|||
const wrapHandler = createIpcWrapper('IPC:editor');
|
||||
const log = createLogger('IPC:editor');
|
||||
|
||||
const MISSING_PROJECT_PATH_ERROR_CODES = new Set(['ENOENT', 'ENOTDIR']);
|
||||
|
||||
function getFileSystemErrorCode(error: unknown): string | null {
|
||||
if (typeof error !== 'object' || error === null || !('code' in error)) {
|
||||
return null;
|
||||
}
|
||||
const code = (error as { code?: unknown }).code;
|
||||
return typeof code === 'string' ? code : null;
|
||||
}
|
||||
|
||||
function isMissingProjectPathError(error: unknown): boolean {
|
||||
return MISSING_PROJECT_PATH_ERROR_CODES.has(getFileSystemErrorCode(error) ?? '');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Handlers
|
||||
// =============================================================================
|
||||
|
|
@ -316,7 +330,15 @@ async function handleProjectListFiles(
|
|||
throw new Error('projectPath is required');
|
||||
}
|
||||
const normalized = path.resolve(projectPath);
|
||||
await fs.access(normalized);
|
||||
const stat = await fs.stat(normalized).catch((error: unknown) => {
|
||||
if (isMissingProjectPathError(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
if (!stat?.isDirectory()) {
|
||||
return [];
|
||||
}
|
||||
return fileSearchService.listFiles(normalized);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ import {
|
|||
buildStandaloneSlashCommandMeta,
|
||||
parseStandaloneSlashCommand,
|
||||
} from '@shared/utils/slashCommands';
|
||||
import { normalizeTeamMemberMcpPolicy } from '@shared/utils/teamMemberMcpPolicy';
|
||||
import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||
import crypto from 'crypto';
|
||||
import { app, BrowserWindow, type IpcMain, type IpcMainInvokeEvent, Notification } from 'electron';
|
||||
|
|
@ -222,6 +223,7 @@ import type {
|
|||
TeamMessageNotificationData,
|
||||
TeamProviderBackendId,
|
||||
TeamProviderId,
|
||||
TeamProvisioningModelCheckRequest,
|
||||
TeamProvisioningModelVerificationMode,
|
||||
TeamProvisioningPrepareResult,
|
||||
TeamProvisioningProgress,
|
||||
|
|
@ -1563,6 +1565,7 @@ interface RuntimeRosterMutationMember {
|
|||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
mcpPolicy?: ReturnType<typeof normalizeTeamMemberMcpPolicy>;
|
||||
removedAt?: number | string | null;
|
||||
}
|
||||
|
||||
|
|
@ -1621,7 +1624,9 @@ function didOpenCodeRosterMemberChange(
|
|||
) ||
|
||||
(previous.model?.trim() || undefined) !== (next.model?.trim() || undefined) ||
|
||||
previous.effort !== next.effort ||
|
||||
previous.fastMode !== next.fastMode
|
||||
previous.fastMode !== next.fastMode ||
|
||||
JSON.stringify(normalizeTeamMemberMcpPolicy(previous.mcpPolicy)) !==
|
||||
JSON.stringify(normalizeTeamMemberMcpPolicy(next.mcpPolicy))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1660,6 +1665,7 @@ function toRollbackReplaceMembersRequest(members: RuntimeRosterMutationMember[])
|
|||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
mcpPolicy?: ReturnType<typeof normalizeTeamMemberMcpPolicy>;
|
||||
}[];
|
||||
} {
|
||||
return {
|
||||
|
|
@ -1675,6 +1681,7 @@ function toRollbackReplaceMembersRequest(members: RuntimeRosterMutationMember[])
|
|||
model: member.model?.trim() || undefined,
|
||||
effort: member.effort,
|
||||
fastMode: member.fastMode,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(member.mcpPolicy),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -1884,6 +1891,7 @@ async function validateProvisioningRequest(
|
|||
model: typeof model === 'string' ? model.trim() || undefined : undefined,
|
||||
effort: effortValidation.value,
|
||||
fastMode: fastModeValidation.value,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy((member as { mcpPolicy?: unknown }).mcpPolicy),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2366,7 +2374,8 @@ async function handlePrepareProvisioning(
|
|||
providerIds: unknown,
|
||||
selectedModels: unknown,
|
||||
limitContext: unknown,
|
||||
modelVerificationMode: unknown
|
||||
modelVerificationMode: unknown,
|
||||
selectedModelChecks: unknown
|
||||
): Promise<IpcResult<TeamProvisioningPrepareResult>> {
|
||||
let validatedCwd: string | undefined;
|
||||
let validatedProviderId: TeamLaunchRequest['providerId'];
|
||||
|
|
@ -2374,6 +2383,7 @@ async function handlePrepareProvisioning(
|
|||
let validatedSelectedModels: string[] | undefined;
|
||||
let validatedLimitContext: boolean | undefined;
|
||||
let validatedModelVerificationMode: TeamProvisioningModelVerificationMode | undefined;
|
||||
let validatedSelectedModelChecks: TeamProvisioningModelCheckRequest[] | undefined;
|
||||
if (cwd !== undefined) {
|
||||
if (typeof cwd !== 'string' || cwd.trim().length === 0) {
|
||||
return { success: false, error: 'cwd must be a non-empty string' };
|
||||
|
|
@ -2436,6 +2446,51 @@ async function handlePrepareProvisioning(
|
|||
}
|
||||
validatedModelVerificationMode = modelVerificationMode;
|
||||
}
|
||||
if (selectedModelChecks !== undefined) {
|
||||
if (!Array.isArray(selectedModelChecks)) {
|
||||
return { success: false, error: 'selectedModelChecks must be an array when provided' };
|
||||
}
|
||||
const normalized: TeamProvisioningModelCheckRequest[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const entry of selectedModelChecks) {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return { success: false, error: 'selectedModelChecks entries must be objects' };
|
||||
}
|
||||
const rawEntry = entry as {
|
||||
providerId?: unknown;
|
||||
model?: unknown;
|
||||
effort?: unknown;
|
||||
};
|
||||
if (!isTeamProviderId(rawEntry.providerId)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'selectedModelChecks entries must include a valid providerId',
|
||||
};
|
||||
}
|
||||
if (typeof rawEntry.model !== 'string' || rawEntry.model.trim().length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'selectedModelChecks entries must include a non-empty model',
|
||||
};
|
||||
}
|
||||
const effortValidation = parseOptionalTeamEffort(rawEntry.effort, rawEntry.providerId);
|
||||
if (!effortValidation.valid) {
|
||||
return { success: false, error: `selectedModelChecks ${effortValidation.error}` };
|
||||
}
|
||||
const model = rawEntry.model.trim();
|
||||
const key = `${rawEntry.providerId}\n${model}\n${effortValidation.value ?? ''}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
normalized.push({
|
||||
providerId: rawEntry.providerId,
|
||||
model,
|
||||
...(effortValidation.value ? { effort: effortValidation.value } : {}),
|
||||
});
|
||||
}
|
||||
validatedSelectedModelChecks = normalized;
|
||||
}
|
||||
return wrapTeamHandler('prepareProvisioning', () =>
|
||||
getTeamProvisioningService().prepareForProvisioning(validatedCwd, {
|
||||
providerId: validatedProviderId,
|
||||
|
|
@ -2443,6 +2498,7 @@ async function handlePrepareProvisioning(
|
|||
modelIds: validatedSelectedModels,
|
||||
limitContext: validatedLimitContext,
|
||||
modelVerificationMode: validatedModelVerificationMode,
|
||||
modelChecks: validatedSelectedModelChecks,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -4246,7 +4302,7 @@ async function handleAddMember(
|
|||
if (!payload || typeof payload !== 'object') {
|
||||
return { success: false, error: 'Invalid payload' };
|
||||
}
|
||||
const { name, role, workflow, isolation, providerId, model } = payload as {
|
||||
const { name, role, workflow, isolation, providerId, model, mcpPolicy } = payload as {
|
||||
name?: unknown;
|
||||
role?: unknown;
|
||||
workflow?: unknown;
|
||||
|
|
@ -4254,6 +4310,7 @@ async function handleAddMember(
|
|||
providerId?: unknown;
|
||||
model?: unknown;
|
||||
effort?: unknown;
|
||||
mcpPolicy?: unknown;
|
||||
};
|
||||
const vName = validateTeammateName(name);
|
||||
if (!vName.valid) return { success: false, error: vName.error ?? 'Invalid member name' };
|
||||
|
|
@ -4302,6 +4359,7 @@ async function handleAddMember(
|
|||
providerId: providerValidation.value,
|
||||
model: typeof model === 'string' ? model.trim() || undefined : undefined,
|
||||
effort: effortValidation.value,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(mcpPolicy),
|
||||
});
|
||||
invalidateTeamRosterSnapshotCaches(tn);
|
||||
|
||||
|
|
@ -4382,6 +4440,7 @@ async function handleReplaceMembers(
|
|||
model?: string;
|
||||
effort?: EffortLevel;
|
||||
fastMode?: TeamFastMode;
|
||||
mcpPolicy?: ReturnType<typeof normalizeTeamMemberMcpPolicy>;
|
||||
}[] = [];
|
||||
for (const item of payload.members) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
|
|
@ -4397,6 +4456,7 @@ async function handleReplaceMembers(
|
|||
model?: unknown;
|
||||
effort?: unknown;
|
||||
fastMode?: unknown;
|
||||
mcpPolicy?: unknown;
|
||||
};
|
||||
const vName = validateTeammateName(m.name);
|
||||
if (!vName.valid) return { success: false, error: vName.error ?? 'Invalid member name' };
|
||||
|
|
@ -4449,6 +4509,7 @@ async function handleReplaceMembers(
|
|||
model: typeof m.model === 'string' ? m.model.trim() || undefined : undefined,
|
||||
effort: effortValidation.value,
|
||||
fastMode: fastModeValidation.value,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(m.mcpPolicy),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ import type { InstalledMcpEntry } from '@shared/types/extensions';
|
|||
|
||||
const logger = createLogger('Extensions:McpConfigStateReader');
|
||||
|
||||
export interface ConfiguredMcpEntry extends InstalledMcpEntry {
|
||||
scope: 'local' | 'user' | 'project';
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class McpConfigStateReader {
|
||||
async readInstalled(projectPath?: string): Promise<InstalledMcpEntry[]> {
|
||||
const entries: InstalledMcpEntry[] = [];
|
||||
|
|
@ -23,6 +28,20 @@ export class McpConfigStateReader {
|
|||
return entries;
|
||||
}
|
||||
|
||||
async readConfigured(projectPath?: string): Promise<ConfiguredMcpEntry[]> {
|
||||
const entries: ConfiguredMcpEntry[] = [];
|
||||
const claudeConfig = await this.readClaudeConfig();
|
||||
|
||||
entries.push(...this.readConfiguredMcpServersFromConfig(claudeConfig?.mcpServers, 'user'));
|
||||
|
||||
if (projectPath) {
|
||||
entries.push(...this.readLocalConfiguredMcpServers(claudeConfig, projectPath));
|
||||
entries.push(...(await this.readProjectConfiguredMcpServers(projectPath)));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async readClaudeConfig(): Promise<Record<string, unknown> | null> {
|
||||
const configPath = path.join(getHomeDir(), '.claude.json');
|
||||
try {
|
||||
|
|
@ -45,6 +64,15 @@ export class McpConfigStateReader {
|
|||
config: Record<string, unknown> | null,
|
||||
projectPath: string
|
||||
): InstalledMcpEntry[] {
|
||||
return this.readLocalConfiguredMcpServers(config, projectPath).map(
|
||||
({ config: _config, ...entry }) => entry
|
||||
);
|
||||
}
|
||||
|
||||
private readLocalConfiguredMcpServers(
|
||||
config: Record<string, unknown> | null,
|
||||
projectPath: string
|
||||
): ConfiguredMcpEntry[] {
|
||||
const projects =
|
||||
config && typeof config.projects === 'object' && config.projects
|
||||
? (config.projects as Record<string, unknown>)
|
||||
|
|
@ -53,7 +81,7 @@ export class McpConfigStateReader {
|
|||
projects && typeof projects[projectPath] === 'object' && projects[projectPath]
|
||||
? (projects[projectPath] as Record<string, unknown>)
|
||||
: null;
|
||||
return this.readMcpServersFromConfig(projectConfig?.mcpServers, 'local');
|
||||
return this.readConfiguredMcpServersFromConfig(projectConfig?.mcpServers, 'local');
|
||||
}
|
||||
|
||||
private async readProjectMcpServers(projectPath: string): Promise<InstalledMcpEntry[]> {
|
||||
|
|
@ -61,6 +89,13 @@ export class McpConfigStateReader {
|
|||
return this.readMcpServersFromFile(configPath, 'project');
|
||||
}
|
||||
|
||||
private async readProjectConfiguredMcpServers(
|
||||
projectPath: string
|
||||
): Promise<ConfiguredMcpEntry[]> {
|
||||
const configPath = path.join(projectPath, '.mcp.json');
|
||||
return this.readConfiguredMcpServersFromFile(configPath, 'project');
|
||||
}
|
||||
|
||||
private readMcpServersFromConfig(
|
||||
value: unknown,
|
||||
scope: 'user' | 'project' | 'local'
|
||||
|
|
@ -82,14 +117,47 @@ export class McpConfigStateReader {
|
|||
});
|
||||
}
|
||||
|
||||
private readConfiguredMcpServersFromConfig(
|
||||
value: unknown,
|
||||
scope: 'user' | 'project' | 'local'
|
||||
): ConfiguredMcpEntry[] {
|
||||
const mcpServers =
|
||||
value && typeof value === 'object' ? (value as Record<string, unknown>) : null;
|
||||
if (!mcpServers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(mcpServers)
|
||||
.filter((entry): entry is [string, Record<string, unknown>] => {
|
||||
const [, config] = entry;
|
||||
return Boolean(config && typeof config === 'object' && !Array.isArray(config));
|
||||
})
|
||||
.map(([name, config]): ConfiguredMcpEntry => {
|
||||
let transport: string | undefined;
|
||||
if (typeof config.command === 'string') transport = 'stdio';
|
||||
else if (typeof config.url === 'string') transport = 'http';
|
||||
|
||||
return { name, scope, transport, config: { ...config } };
|
||||
});
|
||||
}
|
||||
|
||||
private async readMcpServersFromFile(
|
||||
filePath: string,
|
||||
scope: 'user' | 'project'
|
||||
): Promise<InstalledMcpEntry[]> {
|
||||
return (await this.readConfiguredMcpServersFromFile(filePath, scope)).map(
|
||||
({ config: _config, ...entry }) => entry
|
||||
);
|
||||
}
|
||||
|
||||
private async readConfiguredMcpServersFromFile(
|
||||
filePath: string,
|
||||
scope: 'user' | 'project'
|
||||
): Promise<ConfiguredMcpEntry[]> {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf-8');
|
||||
const json = JSON.parse(raw) as Record<string, unknown>;
|
||||
return this.readMcpServersFromConfig(json.mcpServers, scope);
|
||||
return this.readConfiguredMcpServersFromConfig(json.mcpServers, scope);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -734,6 +734,38 @@ export class CliInstallerService {
|
|||
};
|
||||
}
|
||||
|
||||
private getLatestProviderStatusForModelVerification(
|
||||
providerId: CliProviderId,
|
||||
binaryPath: string,
|
||||
installedVersion: string | null
|
||||
): CliProviderStatus | null {
|
||||
const snapshot = this.latestStatusSnapshot;
|
||||
if (
|
||||
!snapshot ||
|
||||
snapshot.flavor !== 'agent_teams_orchestrator' ||
|
||||
snapshot.binaryPath !== binaryPath ||
|
||||
snapshot.installedVersion !== installedVersion
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provider =
|
||||
cloneCliInstallationStatus(snapshot).providers.find(
|
||||
(candidate) => candidate.providerId === providerId
|
||||
) ?? null;
|
||||
if (
|
||||
!provider ||
|
||||
provider.models.length <= 0 ||
|
||||
provider.supported !== true ||
|
||||
provider.authenticated !== true ||
|
||||
provider.capabilities.oneShot !== true
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public: getStatus
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -844,10 +876,12 @@ export class CliInstallerService {
|
|||
return nextProviderStatus;
|
||||
}
|
||||
|
||||
const providerStatus = await this.multimodelBridgeService.getProviderStatus(
|
||||
binaryPath,
|
||||
providerId
|
||||
);
|
||||
const providerStatus =
|
||||
this.getLatestProviderStatusForModelVerification(
|
||||
providerId,
|
||||
binaryPath,
|
||||
versionProbe.version
|
||||
) ?? (await this.multimodelBridgeService.getProviderStatus(binaryPath, providerId));
|
||||
const nextProviderStatus = this.applyProviderModelAvailabilityToProvider(
|
||||
binaryPath,
|
||||
versionProbe.version,
|
||||
|
|
|
|||
|
|
@ -1193,10 +1193,10 @@ export class NotificationManager extends EventEmitter {
|
|||
logger.debug(`[test-notification] creating Notification (platform=${process.platform})`);
|
||||
const notification = new NotificationClass({
|
||||
title: 'Test Notification',
|
||||
...(isMac ? { subtitle: 'Agent Teams UI' } : {}),
|
||||
...(isMac ? { subtitle: 'Agent Teams AI' } : {}),
|
||||
body: isMac
|
||||
? 'Notifications are working correctly!'
|
||||
: 'Agent Teams UI\nNotifications are working correctly!',
|
||||
: 'Agent Teams AI\nNotifications are working correctly!',
|
||||
...(iconPath ? { icon: iconPath } : {}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { execCli } from '@main/utils/childProcess';
|
||||
import { buildMergedCliPath } from '@main/utils/cliPathMerge';
|
||||
import { getAppDataPath } from '@main/utils/pathDecoder';
|
||||
import { safeSendToRenderer } from '@main/utils/safeWebContentsSend';
|
||||
import { getCachedShellEnv, resolveInteractiveShellEnvBestEffort } from '@main/utils/shellEnv';
|
||||
|
|
@ -142,6 +143,7 @@ function resolvePathOpenCodeBinary(
|
|||
const pathEntries = [
|
||||
...additionalEnvSources.flatMap((env) => splitPathEnv(env?.PATH)),
|
||||
...splitPathEnv(shellEnv.PATH),
|
||||
...splitPathEnv(buildMergedCliPath()),
|
||||
...splitPathEnv(process.env.PATH),
|
||||
];
|
||||
const seen = new Set<string>();
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export class CodexAppServerSessionFactory {
|
|||
{
|
||||
clientInfo: {
|
||||
name: 'agent-teams-ai',
|
||||
title: 'Agent Teams UI',
|
||||
title: 'Agent Teams AI',
|
||||
version: '0.1.0',
|
||||
},
|
||||
capabilities: {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,16 @@ import { buildMergedCliPath } from '@main/utils/cliPathMerge';
|
|||
import { getCachedShellEnv } from '@main/utils/shellEnv';
|
||||
|
||||
const CACHE_VERIFY_TTL_MS = 30_000;
|
||||
const STALE_POSITIVE_CACHE_TTL_MS = 5 * 60_000;
|
||||
const VERSION_CACHE_TTL_MS = 30_000;
|
||||
const BINARY_LAUNCH_VERIFY_TIMEOUT_MS = 3_000;
|
||||
|
||||
let cachedBinaryPath: string | null | undefined;
|
||||
let cacheVerifiedAt = 0;
|
||||
let cacheLaunchVerifiedAt = 0;
|
||||
let resolveInFlight: Promise<string | null> | null = null;
|
||||
let cachedMissHadShellEnv = false;
|
||||
let cachedPositiveIsStale = false;
|
||||
const versionCache = new Map<string, { version: string | null; observedAt: number }>();
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
|
|
@ -117,29 +120,54 @@ async function verifyBinary(candidate: string): Promise<string | null> {
|
|||
return null;
|
||||
}
|
||||
|
||||
async function canReuseStalePositiveBinary(
|
||||
candidate: string | null,
|
||||
launchVerifiedAt: number
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
!candidate ||
|
||||
launchVerifiedAt <= 0 ||
|
||||
Date.now() - launchVerifiedAt > STALE_POSITIVE_CACHE_TTL_MS
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fileExists(candidate);
|
||||
}
|
||||
|
||||
export class CodexBinaryResolver {
|
||||
static clearCache(): void {
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
resolveInFlight = null;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
versionCache.clear();
|
||||
}
|
||||
|
||||
static async resolve(): Promise<string | null> {
|
||||
let stalePositiveBinaryPath: string | null = null;
|
||||
let stalePositiveLaunchVerifiedAt = 0;
|
||||
|
||||
if (cachedBinaryPath !== undefined) {
|
||||
if (cachedBinaryPath === null) {
|
||||
if (!cachedMissHadShellEnv && getCachedShellEnv() !== null) {
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
} else {
|
||||
const verifiedAppManagedBinaryPath =
|
||||
await resolveVerifiedAppManagedCodexRuntimeBinaryPath();
|
||||
if (verifiedAppManagedBinaryPath) {
|
||||
const now = Date.now();
|
||||
cachedBinaryPath = verifiedAppManagedBinaryPath;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheVerifiedAt = now;
|
||||
cacheLaunchVerifiedAt = now;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
return verifiedAppManagedBinaryPath;
|
||||
}
|
||||
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
||||
|
|
@ -147,22 +175,36 @@ export class CodexBinaryResolver {
|
|||
}
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
}
|
||||
} else {
|
||||
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
||||
const now = Date.now();
|
||||
const stalePositiveIsStillAllowed =
|
||||
!cachedPositiveIsStale || now - cacheLaunchVerifiedAt <= STALE_POSITIVE_CACHE_TTL_MS;
|
||||
if (now - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS && stalePositiveIsStillAllowed) {
|
||||
return cachedBinaryPath;
|
||||
}
|
||||
|
||||
const verified = await verifyBinary(cachedBinaryPath);
|
||||
const cachedPositiveBinaryPath = cachedBinaryPath;
|
||||
const cachedPositiveLaunchVerifiedAt = cacheLaunchVerifiedAt;
|
||||
const verified = await verifyBinary(cachedPositiveBinaryPath);
|
||||
if (verified) {
|
||||
cacheVerifiedAt = Date.now();
|
||||
const verifiedAt = Date.now();
|
||||
cacheVerifiedAt = verifiedAt;
|
||||
cacheLaunchVerifiedAt = verifiedAt;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
return verified;
|
||||
}
|
||||
|
||||
stalePositiveBinaryPath = cachedPositiveBinaryPath;
|
||||
stalePositiveLaunchVerifiedAt = cachedPositiveLaunchVerifiedAt;
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedPositiveIsStale = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +214,20 @@ export class CodexBinaryResolver {
|
|||
});
|
||||
}
|
||||
|
||||
return resolveInFlight;
|
||||
const resolved = await resolveInFlight;
|
||||
if (
|
||||
!resolved &&
|
||||
(await canReuseStalePositiveBinary(stalePositiveBinaryPath, stalePositiveLaunchVerifiedAt))
|
||||
) {
|
||||
cachedBinaryPath = stalePositiveBinaryPath;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheLaunchVerifiedAt = stalePositiveLaunchVerifiedAt;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = true;
|
||||
return stalePositiveBinaryPath;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static async runResolve(): Promise<string | null> {
|
||||
|
|
@ -187,16 +242,21 @@ export class CodexBinaryResolver {
|
|||
for (const candidate of candidates) {
|
||||
const resolved = await verifyBinary(candidate);
|
||||
if (resolved) {
|
||||
const now = Date.now();
|
||||
cachedBinaryPath = resolved;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheVerifiedAt = now;
|
||||
cacheLaunchVerifiedAt = now;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
cachedBinaryPath = null;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedMissHadShellEnv = getCachedShellEnv() !== null;
|
||||
cachedPositiveIsStale = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
// @vitest-environment node
|
||||
import { chmod, mkdtemp, rm, unlink, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@main/utils/cliPathMerge', () => ({
|
||||
buildMergedCliPath: () => process.env.PATH ?? '',
|
||||
}));
|
||||
|
||||
vi.mock('@main/utils/shellEnv', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@main/utils/shellEnv')>();
|
||||
return {
|
||||
...actual,
|
||||
getCachedShellEnv: () => null,
|
||||
};
|
||||
});
|
||||
|
||||
const originalPath = process.env.PATH;
|
||||
const originalCodexCliPath = process.env.CODEX_CLI_PATH;
|
||||
const originalFakeFailFile = process.env.CODEX_FAKE_CODEX_FAIL_FILE;
|
||||
const describePosix = process.platform === 'win32' ? describe.skip : describe;
|
||||
const LIVE_CODEX_BINARY_SMOKE = process.env.LIVE_CODEX_BINARY_RESOLVER_SMOKE === '1';
|
||||
const describeLive = LIVE_CODEX_BINARY_SMOKE ? describe : describe.skip;
|
||||
const BASE_TIME_MS = 1_767_225_600_000;
|
||||
|
||||
let tempDirs: string[] = [];
|
||||
|
||||
async function clearResolverCache(): Promise<void> {
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
}
|
||||
|
||||
async function createFakeCodexBinary(): Promise<{
|
||||
binaryPath: string;
|
||||
failMarkerPath: string;
|
||||
}> {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'codex-binary-resolver-real-'));
|
||||
tempDirs.push(tempDir);
|
||||
const binaryPath = path.join(tempDir, 'codex');
|
||||
const failMarkerPath = path.join(tempDir, 'fail');
|
||||
await writeFile(
|
||||
binaryPath,
|
||||
[
|
||||
'#!/bin/sh',
|
||||
'if [ -n "$CODEX_FAKE_CODEX_FAIL_FILE" ] && [ -f "$CODEX_FAKE_CODEX_FAIL_FILE" ]; then',
|
||||
' echo "fake codex failure" >&2',
|
||||
' exit 42',
|
||||
'fi',
|
||||
'if [ "$1" = "--version" ]; then',
|
||||
' echo "codex-cli 99.0.0"',
|
||||
' exit 0',
|
||||
'fi',
|
||||
'echo "unexpected args: $*" >&2',
|
||||
'exit 2',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
await chmod(binaryPath, 0o755);
|
||||
process.env.PATH = tempDir;
|
||||
return { binaryPath, failMarkerPath };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
process.env.PATH = originalPath;
|
||||
process.env.CODEX_CLI_PATH = originalCodexCliPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = originalFakeFailFile;
|
||||
await clearResolverCache();
|
||||
await Promise.all(tempDirs.map((tempDir) => rm(tempDir, { recursive: true, force: true })));
|
||||
tempDirs = [];
|
||||
});
|
||||
|
||||
describePosix('CodexBinaryResolver real filesystem/process smoke', () => {
|
||||
it('resolves an explicit executable through real fs access and execFile', async () => {
|
||||
const { binaryPath, failMarkerPath } = await createFakeCodexBinary();
|
||||
process.env.CODEX_CLI_PATH = binaryPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = failMarkerPath;
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
await expect(CodexBinaryResolver.resolveVersion(binaryPath)).resolves.toBe('99.0.0');
|
||||
});
|
||||
|
||||
it('keeps a recent real executable during transient launch failure, then expires it', async () => {
|
||||
const { binaryPath, failMarkerPath } = await createFakeCodexBinary();
|
||||
process.env.CODEX_CLI_PATH = binaryPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = failMarkerPath;
|
||||
const nowSpy = vi.spyOn(Date, 'now');
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
|
||||
await writeFile(failMarkerPath, 'fail', 'utf8');
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS + 30_001);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS + 300_001);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('does not keep a recent real executable after it is removed', async () => {
|
||||
const { binaryPath, failMarkerPath } = await createFakeCodexBinary();
|
||||
process.env.CODEX_CLI_PATH = binaryPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = failMarkerPath;
|
||||
const nowSpy = vi.spyOn(Date, 'now');
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
|
||||
await unlink(binaryPath);
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS + 30_001);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describeLive('CodexBinaryResolver live local Codex smoke', () => {
|
||||
it('resolves and versions the current local Codex binary', async () => {
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
const binaryPath = await CodexBinaryResolver.resolve();
|
||||
expect(binaryPath).toEqual(expect.any(String));
|
||||
const version = await CodexBinaryResolver.resolveVersion(binaryPath);
|
||||
expect(version).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
|
|
@ -327,6 +327,137 @@ describe('CodexBinaryResolver', () => {
|
|||
expect(buildEnrichedEnvMock).toHaveBeenCalledWith(codexShim);
|
||||
});
|
||||
|
||||
it('reuses a recent known-good binary when revalidation transiently fails', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
process.env.PATH = '/usr/local/bin:/usr/bin:/bin';
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
let canLaunch = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === codexShim) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
execCliMock.mockImplementation(() => {
|
||||
if (canLaunch) {
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.130.0', stderr: '' });
|
||||
}
|
||||
return Promise.reject(new Error('codex --version timed out'));
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
canLaunch = false;
|
||||
vi.advanceTimersByTime(30_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
});
|
||||
|
||||
it('expires stale known-good reuse from the last real launch verification', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
process.env.PATH = '/usr/local/bin:/usr/bin:/bin';
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
let canLaunch = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === codexShim) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
execCliMock.mockImplementation(() => {
|
||||
if (canLaunch) {
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.130.0', stderr: '' });
|
||||
}
|
||||
return Promise.reject(new Error('codex --version timed out'));
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
canLaunch = false;
|
||||
vi.advanceTimersByTime(290_000);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
vi.advanceTimersByTime(10_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('prefers a newly resolved binary over stale known-good reuse', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
const oldCodexShim = path.posix.join('/old/bin', 'codex');
|
||||
const newCodexShim = path.posix.join('/new/bin', 'codex');
|
||||
process.env.PATH = '/old/bin:/usr/bin:/bin';
|
||||
let oldCanLaunch = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === oldCodexShim || filePath === newCodexShim) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
execCliMock.mockImplementation((binaryPath) => {
|
||||
if (binaryPath === oldCodexShim) {
|
||||
if (oldCanLaunch) {
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.130.0', stderr: '' });
|
||||
}
|
||||
return Promise.reject(new Error('old codex --version timed out'));
|
||||
}
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.131.0', stderr: '' });
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(oldCodexShim);
|
||||
|
||||
oldCanLaunch = false;
|
||||
process.env.PATH = '/new/bin:/usr/bin:/bin';
|
||||
vi.advanceTimersByTime(30_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(newCodexShim);
|
||||
});
|
||||
|
||||
it('does not reuse a recent known-good binary after the file disappears', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
process.env.PATH = '/usr/local/bin:/usr/bin:/bin';
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
let filePresent = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === codexShim && filePresent) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
filePresent = false;
|
||||
vi.advanceTimersByTime(30_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('uses enriched env for Codex version probes', async () => {
|
||||
setPlatform('darwin');
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
|
|
|
|||
|
|
@ -679,6 +679,11 @@ export class ClaudeMultimodelBridgeService {
|
|||
|
||||
private readonly providerStatusHydrationGenerations = new Map<CliProviderId, number>();
|
||||
|
||||
private readonly providerStatusHydrationInFlight = new Map<
|
||||
CliProviderId,
|
||||
{ readonly generation: number; readonly promise: Promise<CliProviderStatus> }
|
||||
>();
|
||||
|
||||
private beginProviderStatusHydration(providerIds: readonly CliProviderId[]): number {
|
||||
const generation = ++this.providerStatusHydrationGeneration;
|
||||
for (const providerId of providerIds) {
|
||||
|
|
@ -691,6 +696,38 @@ export class ClaudeMultimodelBridgeService {
|
|||
return this.providerStatusHydrationGenerations.get(providerId) === generation;
|
||||
}
|
||||
|
||||
private getProviderCatalogHydration(
|
||||
binaryPath: string,
|
||||
providerId: CliProviderId,
|
||||
generation: number
|
||||
): Promise<CliProviderStatus | null> {
|
||||
const inFlight = this.providerStatusHydrationInFlight.get(providerId);
|
||||
if (inFlight) {
|
||||
if (inFlight.generation === generation) {
|
||||
return inFlight.promise;
|
||||
}
|
||||
|
||||
return inFlight.promise
|
||||
.catch(() => undefined)
|
||||
.then(() => {
|
||||
if (!this.isProviderStatusHydrationCurrent(providerId, generation)) {
|
||||
return null;
|
||||
}
|
||||
return this.getProviderCatalogHydration(binaryPath, providerId, generation);
|
||||
});
|
||||
}
|
||||
|
||||
const request = this.getProviderStatusFromScopedRuntimeStatus(binaryPath, providerId).finally(
|
||||
() => {
|
||||
if (this.providerStatusHydrationInFlight.get(providerId)?.promise === request) {
|
||||
this.providerStatusHydrationInFlight.delete(providerId);
|
||||
}
|
||||
}
|
||||
);
|
||||
this.providerStatusHydrationInFlight.set(providerId, { generation, promise: request });
|
||||
return request;
|
||||
}
|
||||
|
||||
private async buildCliEnv(
|
||||
binaryPath: string
|
||||
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||
|
|
@ -708,18 +745,23 @@ export class ClaudeMultimodelBridgeService {
|
|||
});
|
||||
}
|
||||
|
||||
private isUnifiedRuntimeUnsupported(error: unknown): boolean {
|
||||
private isRuntimeStatusCompatibilityError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const lower = message.toLowerCase();
|
||||
return (
|
||||
lower.includes('unknown command') ||
|
||||
lower.includes('unknown option') ||
|
||||
lower.includes('no such command') ||
|
||||
lower.includes('did you mean') ||
|
||||
lower.includes('runtime status')
|
||||
lower.includes('did you mean')
|
||||
);
|
||||
}
|
||||
|
||||
private isUnifiedRuntimeUnsupported(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const lower = message.toLowerCase();
|
||||
return this.isRuntimeStatusCompatibilityError(error) || lower.includes('runtime status');
|
||||
}
|
||||
|
||||
private mapRuntimeProviderStatus(
|
||||
providerId: CliProviderId,
|
||||
runtimeStatus: NonNullable<UnifiedRuntimeStatusResponse['providers']>[string] | undefined
|
||||
|
|
@ -961,8 +1003,11 @@ export class ClaudeMultimodelBridgeService {
|
|||
continue;
|
||||
}
|
||||
|
||||
void this.getProviderStatusFromScopedRuntimeStatus(binaryPath, liveProvider.providerId)
|
||||
void this.getProviderCatalogHydration(binaryPath, liveProvider.providerId, generation)
|
||||
.then((hydratedProvider) => {
|
||||
if (!hydratedProvider) {
|
||||
return;
|
||||
}
|
||||
if (!this.isProviderStatusHydrationCurrent(liveProvider.providerId, generation)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1100,8 +1145,11 @@ export class ClaudeMultimodelBridgeService {
|
|||
summary: true,
|
||||
});
|
||||
if (provider.runtimeCapabilities?.modelCatalog?.dynamic === true && onCatalogUpdate) {
|
||||
void this.getProviderStatusFromScopedRuntimeStatus(binaryPath, provider.providerId)
|
||||
void this.getProviderCatalogHydration(binaryPath, provider.providerId, generation)
|
||||
.then((hydratedProvider) => {
|
||||
if (!hydratedProvider) {
|
||||
return;
|
||||
}
|
||||
if (!this.isProviderStatusHydrationCurrent(provider.providerId, generation)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1124,24 +1172,35 @@ export class ClaudeMultimodelBridgeService {
|
|||
}
|
||||
return provider;
|
||||
} catch (error) {
|
||||
if (!this.isUnifiedRuntimeUnsupported(error)) {
|
||||
if (providerId === 'gemini' && this.isRuntimeStatusCompatibilityError(error)) {
|
||||
return this.buildGeminiStatus(binaryPath);
|
||||
}
|
||||
|
||||
if (this.isRuntimeStatusCompatibilityError(error)) {
|
||||
logger.warn(
|
||||
`Provider-scoped summary runtime status unavailable for ${providerId}, falling back to full probe: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
try {
|
||||
return await this.getProviderStatusFromScopedRuntimeStatus(binaryPath, providerId);
|
||||
} catch (fullError) {
|
||||
logger.warn(
|
||||
`Provider-scoped full runtime status unavailable for ${providerId}, returning scoped error: ${
|
||||
fullError instanceof Error ? fullError.message : String(fullError)
|
||||
}`
|
||||
);
|
||||
return createRuntimeStatusErrorProviderStatus(providerId, fullError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (providerId === 'gemini') {
|
||||
return this.buildGeminiStatus(binaryPath);
|
||||
logger.warn(
|
||||
`Provider-scoped summary runtime status unavailable for ${providerId}, returning scoped error: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
return createRuntimeStatusErrorProviderStatus(providerId, error);
|
||||
}
|
||||
|
||||
const providers = await this.getProviderStatuses(binaryPath);
|
||||
return (
|
||||
providers.find((provider) => provider.providerId === providerId) ??
|
||||
createDefaultProviderStatus(providerId)
|
||||
);
|
||||
}
|
||||
|
||||
async verifyProviderStatus(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRea
|
|||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
import { normalizeTeamMemberMcpPolicy } from '@shared/utils/teamMemberMcpPolicy';
|
||||
import {
|
||||
createCliAutoSuffixNameGuard,
|
||||
createCliProvisionerNameGuard,
|
||||
|
|
@ -559,6 +560,7 @@ export class TeamConfigReader {
|
|||
name: existing?.name ?? name,
|
||||
role: m.role?.trim() || existing?.role,
|
||||
color: m.color?.trim() || existing?.color,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(m.mcpPolicy) ?? existing?.mcpPolicy,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { getReviewStateFromTask } from '@shared/utils/reviewState';
|
|||
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
|
||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
||||
import { normalizeTeamMemberMcpPolicy } from '@shared/utils/teamMemberMcpPolicy';
|
||||
import { parseNumericSuffixName, validateTeamMemberNameFormat } from '@shared/utils/teamMemberName';
|
||||
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||
import { extractToolPreview, formatToolSummaryFromCalls } from '@shared/utils/toolSummary';
|
||||
|
|
@ -998,6 +999,7 @@ export class TeamDataService {
|
|||
model: member.model,
|
||||
effort: member.effort,
|
||||
fastMode: member.fastMode,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(member.mcpPolicy),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
@ -1812,6 +1814,7 @@ export class TeamDataService {
|
|||
providerId: normalizeOptionalTeamProviderId(request.providerId),
|
||||
model: request.model?.trim() || undefined,
|
||||
effort: isTeamEffortLevel(request.effort) ? request.effort : undefined,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(request.mcpPolicy),
|
||||
agentType: 'general-purpose',
|
||||
joinedAt: Date.now(),
|
||||
};
|
||||
|
|
@ -1888,6 +1891,7 @@ export class TeamDataService {
|
|||
member.fastMode === 'inherit' || member.fastMode === 'on' || member.fastMode === 'off'
|
||||
? member.fastMode
|
||||
: undefined,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(member.mcpPolicy),
|
||||
agentType: prev?.agentType ?? 'general-purpose',
|
||||
agentId: isSameActiveMember ? prev?.agentId : undefined,
|
||||
color: prev?.color,
|
||||
|
|
@ -3011,6 +3015,7 @@ export class TeamDataService {
|
|||
model: member.model?.trim() || undefined,
|
||||
effort: isTeamEffortLevel(member.effort) ? member.effort : undefined,
|
||||
fastMode: member.fastMode,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(member.mcpPolicy),
|
||||
agentType: 'general-purpose' as const,
|
||||
joinedAt,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -219,6 +219,35 @@ function firstEvidence(parts: readonly string[], pattern: RegExp): string[] {
|
|||
const WORKSPACE_TRUST_FAILURE_PATTERN =
|
||||
/workspace trust is not accepted|cannot start in headless process runtime because workspace trust|open that workspace once interactively and accept trust|workspace_trust_preflight_not_confirmed|workspace trust was not confirmed|workspace trust preflight blocked launch/i;
|
||||
|
||||
const BOOTSTRAP_TRANSPORT_EVIDENCE_PATTERN = new RegExp(
|
||||
[
|
||||
'mailbox_bootstrap_written',
|
||||
'bootstrap_prompt_observed',
|
||||
'bootstrap_submit_attempted',
|
||||
'bootstrap_submitted',
|
||||
'inbox_poller_ready',
|
||||
'runtime_events_log',
|
||||
].join('|'),
|
||||
'i'
|
||||
);
|
||||
|
||||
const MODEL_NO_BOOTSTRAP_PATTERN = new RegExp(
|
||||
[
|
||||
'did not bootstrap-confirm',
|
||||
'bootstrap unconfirmed',
|
||||
'bootstrap-confirm before timeout',
|
||||
'bootstrap was not confirmed',
|
||||
'bootstrap not confirmed',
|
||||
'check-in not yet received',
|
||||
'bootstrap_stalled',
|
||||
'did not submit bootstrap prompt',
|
||||
'bootstrap_submit_accepted_without_uuid',
|
||||
'timed out waiting for bootstrap_submitted',
|
||||
'last transport stage:\\s*(?:mailbox_bootstrap_written|bootstrap_prompt_observed|bootstrap_submit_attempted|bootstrap_submitted)',
|
||||
].join('|'),
|
||||
'i'
|
||||
);
|
||||
|
||||
export function isWorkspaceTrustLaunchFailureText(value: string): boolean {
|
||||
return WORKSPACE_TRUST_FAILURE_PATTERN.test(value);
|
||||
}
|
||||
|
|
@ -228,6 +257,7 @@ export function classifyLaunchFailureArtifact(
|
|||
): LaunchFailureArtifactClassification {
|
||||
const parts = collectLaunchFailureSearchParts(input);
|
||||
const text = parts.join('\n').toLowerCase();
|
||||
const hasBootstrapTransportEvidence = BOOTSTRAP_TRANSPORT_EVIDENCE_PATTERN.test(text);
|
||||
const candidates: {
|
||||
code: LaunchFailureArtifactClassificationCode;
|
||||
confidence: number;
|
||||
|
|
@ -268,8 +298,7 @@ export function classifyLaunchFailureArtifact(
|
|||
{
|
||||
code: 'model_no_bootstrap',
|
||||
confidence: 0.82,
|
||||
pattern:
|
||||
/did not bootstrap-confirm|bootstrap unconfirmed|bootstrap-confirm before timeout|bootstrap was not confirmed|bootstrap not confirmed|check-in not yet received|bootstrap_stalled/i,
|
||||
pattern: MODEL_NO_BOOTSTRAP_PATTERN,
|
||||
},
|
||||
{
|
||||
code: 'process_exited',
|
||||
|
|
@ -279,6 +308,9 @@ export function classifyLaunchFailureArtifact(
|
|||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.code === 'stdin_missing' && hasBootstrapTransportEvidence) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.pattern.test(text)) {
|
||||
return {
|
||||
code: candidate.code,
|
||||
|
|
@ -305,24 +337,29 @@ export function extractLaunchBootstrapTransportBreadcrumb(
|
|||
];
|
||||
const evidence = firstEvidence(
|
||||
parts,
|
||||
/bootstrap_submit_|last transport stage|no stdin data received|local prompt handler/i
|
||||
/bootstrap_submit_|mailbox_bootstrap_written|bootstrap_prompt_observed|bootstrap_submitted|last transport stage|no stdin data received|local prompt handler/i
|
||||
).map(redactLaunchFailureArtifactText);
|
||||
const retryableRaw = retryableMatches.at(-1)?.[1]?.toLowerCase();
|
||||
return {
|
||||
lastTransportStage: lastStageMatches.at(-1)?.[1]?.trim() ?? null,
|
||||
lastTransportStage: normalizeLastTransportStage(lastStageMatches.at(-1)?.[1]),
|
||||
submitRejected: /bootstrap_submit_rejected|submit rejected by local prompt handler/i.test(
|
||||
combined
|
||||
),
|
||||
retryable: retryableRaw === 'true' ? true : retryableRaw === 'false' ? false : null,
|
||||
noStdinWarning: /no stdin data received|proceeding without it/i.test(combined),
|
||||
bootstrapSubmitted:
|
||||
/(?:event["']?\s*:\s*["']bootstrap_submitted["']|bootstrap_submit_accepted|bootstrap submitted)/i.test(
|
||||
/(?:(?:event|type)["']?\s*[:=]\s*["']bootstrap_submitted["']|bootstrap_submit_accepted|bootstrap submitted)/i.test(
|
||||
combined
|
||||
),
|
||||
evidence,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLastTransportStage(stage: string | undefined): string | null {
|
||||
const normalized = stage?.replace(/\s+Last\s+(?:stderr|stdout):.*$/i, '').trim();
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
async function readBoundedTextFile(sourcePath: string): Promise<{ text?: string; issue?: string }> {
|
||||
try {
|
||||
const stat = await fs.promises.stat(sourcePath);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,17 @@ import {
|
|||
} from '@main/utils/pathDecoder';
|
||||
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
import { resolveTeamMemberMcpScopes } from '@shared/utils/teamMemberMcpPolicy';
|
||||
import { randomUUID } from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { McpConfigStateReader } from '../extensions/runtime/McpConfigStateReader';
|
||||
|
||||
import { atomicWriteAsync } from './atomicWrite';
|
||||
|
||||
import type { TeamMemberMcpPolicy, TeamMemberMcpScope } from '@shared/types';
|
||||
|
||||
export interface McpLaunchSpec {
|
||||
command: string;
|
||||
args: string[];
|
||||
|
|
@ -27,6 +32,10 @@ export interface McpLaunchSpecResolveOptions {
|
|||
onProgress?: (progress: McpLaunchSpecResolveProgress) => void;
|
||||
}
|
||||
|
||||
interface WriteMcpConfigOptions {
|
||||
mcpPolicy?: TeamMemberMcpPolicy;
|
||||
}
|
||||
|
||||
const MCP_SERVER_NAME = 'agent-teams';
|
||||
const MCP_CLAUDE_DIR_ENV = 'AGENT_TEAMS_MCP_CLAUDE_DIR';
|
||||
const logger = createLogger('Service:TeamMcpConfigBuilder');
|
||||
|
|
@ -43,6 +52,8 @@ const MCP_CONFIG_STALE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|||
|
||||
type McpServerConfig = Record<string, unknown>;
|
||||
|
||||
const MCP_CONFIG_SCOPE_PRECEDENCE: readonly TeamMemberMcpScope[] = ['user', 'project', 'local'];
|
||||
|
||||
function isPackagedApp(): boolean {
|
||||
try {
|
||||
const { app } = require('electron') as typeof import('electron');
|
||||
|
|
@ -417,26 +428,42 @@ export async function resolveAgentTeamsMcpLaunchSpec(
|
|||
}
|
||||
|
||||
export class TeamMcpConfigBuilder {
|
||||
async writeConfigFile(_projectPath?: string): Promise<string> {
|
||||
async writeConfigFile(projectPath?: string, options?: WriteMcpConfigOptions): Promise<string>;
|
||||
async writeConfigFile(projectPath?: string, mcpPolicy?: TeamMemberMcpPolicy): Promise<string>;
|
||||
async writeConfigFile(
|
||||
projectPath?: string,
|
||||
optionsOrPolicy?: WriteMcpConfigOptions | TeamMemberMcpPolicy
|
||||
): Promise<string> {
|
||||
const launchSpec = await resolveAgentTeamsMcpLaunchSpec();
|
||||
const configDir = getMcpConfigsBasePath();
|
||||
const configPath = path.join(
|
||||
configDir,
|
||||
`${MCP_CONFIG_PREFIX}${process.pid}-${Date.now()}-${randomUUID()}.json`
|
||||
);
|
||||
const mcpPolicy =
|
||||
optionsOrPolicy && 'mcpPolicy' in optionsOrPolicy
|
||||
? optionsOrPolicy.mcpPolicy
|
||||
: (optionsOrPolicy as TeamMemberMcpPolicy | undefined);
|
||||
// Keep the team bootstrap config minimal: recent Claude sidechain runs can
|
||||
// lose the agent-teams tool surface when we inline large user MCP bundles
|
||||
// into the generated --mcp-config. User/project/local MCP remain loaded
|
||||
// through Claude's native settings sources.
|
||||
const generatedServers: Record<string, McpServerConfig> = {
|
||||
[MCP_SERVER_NAME]: {
|
||||
command: launchSpec.command,
|
||||
args: launchSpec.args,
|
||||
env: {
|
||||
[MCP_CLAUDE_DIR_ENV]: getClaudeBasePath(),
|
||||
},
|
||||
const generatedServers: Record<string, McpServerConfig> = Object.create(null);
|
||||
generatedServers[MCP_SERVER_NAME] = {
|
||||
command: launchSpec.command,
|
||||
args: launchSpec.args,
|
||||
enabled: true,
|
||||
env: {
|
||||
[MCP_CLAUDE_DIR_ENV]: getClaudeBasePath(),
|
||||
},
|
||||
};
|
||||
if (mcpPolicy?.mode === 'strictAllowlist') {
|
||||
for (const [name, config] of Object.entries(
|
||||
await this.readAllowlistedServers(projectPath, mcpPolicy)
|
||||
)) {
|
||||
generatedServers[name] = config;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(configDir, { recursive: true });
|
||||
await atomicWriteAsync(
|
||||
|
|
@ -453,6 +480,47 @@ export class TeamMcpConfigBuilder {
|
|||
return configPath;
|
||||
}
|
||||
|
||||
private async readAllowlistedServers(
|
||||
projectPath: string | undefined,
|
||||
policy: TeamMemberMcpPolicy
|
||||
): Promise<Record<string, McpServerConfig>> {
|
||||
const allowlist = new Set(
|
||||
(policy.serverNames ?? [])
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean)
|
||||
.map((name) => name.toLowerCase())
|
||||
);
|
||||
if (allowlist.size === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const scopes = resolveTeamMemberMcpScopes(policy);
|
||||
const entries = await new McpConfigStateReader().readConfigured(projectPath);
|
||||
const byScope = new Map<TeamMemberMcpScope, typeof entries>();
|
||||
for (const scope of MCP_CONFIG_SCOPE_PRECEDENCE) {
|
||||
byScope.set(scope, []);
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!scopes[entry.scope]) {
|
||||
continue;
|
||||
}
|
||||
byScope.get(entry.scope)?.push(entry);
|
||||
}
|
||||
|
||||
const selected: Record<string, McpServerConfig> = Object.create(null);
|
||||
for (const scope of MCP_CONFIG_SCOPE_PRECEDENCE) {
|
||||
for (const entry of byScope.get(scope) ?? []) {
|
||||
if (entry.name.toLowerCase() === MCP_SERVER_NAME) {
|
||||
continue;
|
||||
}
|
||||
if (allowlist.has(entry.name.toLowerCase())) {
|
||||
selected[entry.name] = entry.config;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
/** Delete a single MCP config file (best-effort). */
|
||||
async removeConfigFile(configPath: string): Promise<void> {
|
||||
for (let attempt = 0; attempt <= MCP_CONFIG_REMOVE_RETRY_DELAYS_MS.length; attempt += 1) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { buildPlannedMemberLaneIdentity } from '@features/team-runtime-lanes';
|
|||
import { getMemberColorByName } from '@shared/constants/memberColors';
|
||||
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
||||
import { normalizeTeamMemberMcpPolicy } from '@shared/utils/teamMemberMcpPolicy';
|
||||
import {
|
||||
createCliAutoSuffixNameGuard,
|
||||
createCliProvisionerNameGuard,
|
||||
|
|
@ -163,6 +164,7 @@ export class TeamMemberResolver {
|
|||
model?: string;
|
||||
effort?: TeamMember['effort'];
|
||||
fastMode?: TeamMember['fastMode'];
|
||||
mcpPolicy?: TeamMember['mcpPolicy'];
|
||||
color?: string;
|
||||
cwd?: string;
|
||||
}
|
||||
|
|
@ -190,6 +192,7 @@ export class TeamMemberResolver {
|
|||
configMember.fastMode === 'off'
|
||||
? configMember.fastMode
|
||||
: undefined,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(configMember.mcpPolicy),
|
||||
color: configMember.color,
|
||||
cwd: configMember.cwd,
|
||||
});
|
||||
|
|
@ -210,6 +213,7 @@ export class TeamMemberResolver {
|
|||
model?: string;
|
||||
effort?: TeamMember['effort'];
|
||||
fastMode?: TeamMember['fastMode'];
|
||||
mcpPolicy?: TeamMember['mcpPolicy'];
|
||||
color?: string;
|
||||
cwd?: string;
|
||||
removedAt?: number;
|
||||
|
|
@ -235,6 +239,7 @@ export class TeamMemberResolver {
|
|||
member.fastMode === 'inherit' || member.fastMode === 'on' || member.fastMode === 'off'
|
||||
? member.fastMode
|
||||
: undefined,
|
||||
mcpPolicy: normalizeTeamMemberMcpPolicy(member.mcpPolicy),
|
||||
color: member.color,
|
||||
cwd: member.cwd,
|
||||
removedAt: member.removedAt,
|
||||
|
|
@ -324,6 +329,7 @@ export class TeamMemberResolver {
|
|||
providerBackendId,
|
||||
model: launchMember?.model ?? configMember?.model ?? metaMember?.model,
|
||||
effort: launchMember?.effort ?? configMember?.effort ?? metaMember?.effort,
|
||||
mcpPolicy: configMember?.mcpPolicy ?? metaMember?.mcpPolicy,
|
||||
selectedFastMode:
|
||||
launchMember?.selectedFastMode ??
|
||||
configMember?.fastMode ??
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue