docs: refresh README
This commit is contained in:
parent
821cda64a8
commit
6c8ad99b06
|
|
@ -11,7 +11,7 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
important:
|
important:
|
||||||
description: 'Mark this nightly as an important update (escalates the in-app restart prompt). Retro-flag an already-published nightly by re-running only the publish-feed job with this input set.'
|
description: "Mark this nightly as an important update (escalates the in-app restart prompt). Retro-flag an already-published nightly by re-running only the publish-feed job with this input set."
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# Generated — never hand-edit; regenerated by `npm run api` / sqlc / openapi-typescript
|
# Generated — never hand-edit; regenerated by `npm run api` / sqlc / openapi-typescript
|
||||||
frontend/src/api/schema.ts
|
frontend/src/api/schema.ts
|
||||||
|
frontend/src/renderer/routeTree.gen.ts
|
||||||
backend/internal/httpd/apispec/openapi.yaml
|
backend/internal/httpd/apispec/openapi.yaml
|
||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
|
|
|
||||||
291
README.md
291
README.md
|
|
@ -1,6 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
<img src="ao-logo.svg" alt="Agent Orchestrator" width="160" height="160" />
|
||||||
<p style="text-align: center;"><img src="ao-logo.svg" alt="Agent Orchestrator" width="200" height="200" style="max-width: 100%; height: auto; margin-left: 50px;" /></p>
|
|
||||||
|
|
||||||
# Agent Orchestrator
|
# Agent Orchestrator
|
||||||
|
|
||||||
|
|
@ -10,12 +9,11 @@
|
||||||
[](https://github.com/AgentWrapper/agent-orchestrator/graphs/contributors)
|
[](https://github.com/AgentWrapper/agent-orchestrator/graphs/contributors)
|
||||||
[](https://x.com/aoagents)
|
[](https://x.com/aoagents)
|
||||||
[](https://discord.com/invite/UZv7JjxbwG)
|
[](https://discord.com/invite/UZv7JjxbwG)
|
||||||
[](https://www.apache.org/licenses/LICENSE-2.0)
|
[](LICENSE)
|
||||||
|
|
||||||
An Agentic IDE that supervises parallel AI coding agents in isolated workspaces, with complete control and automatic feedback loops from CI failures, review comments, and merge conflicts.
|
An Agentic IDE that supervises parallel AI coding agents in isolated workspaces, with complete control and automatic feedback loops from CI failures, review comments, and merge conflicts.
|
||||||
|
|
||||||

|
<img src="docs/assets/readme/dashboard.png" alt="Agent Orchestrator dashboard showing parallel coding agent sessions" width="100%" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -26,8 +24,6 @@ Agent Orchestrator is a meta-harness agent IDE for running AI coding agents in p
|
||||||
|
|
||||||
The agents still do the coding. AO provides the harness around them: isolated workspaces, live terminal access, session state, PR awareness, and automatic loops that send CI failures, review comments, and merge conflicts back to the right agent. Instead of manually coordinating a pile of agent terminals, AO turns parallel agent work into a managed workflow.
|
The agents still do the coding. AO provides the harness around them: isolated workspaces, live terminal access, session state, PR awareness, and automatic loops that send CI failures, review comments, and merge conflicts back to the right agent. Instead of manually coordinating a pile of agent terminals, AO turns parallel agent work into a managed workflow.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why Agent Orchestrator?
|
## Why Agent Orchestrator?
|
||||||
|
|
||||||
AI coding agents become much more useful when they can work in parallel, but parallel work gets messy quickly. Branches overlap, terminals get lost, CI failures need follow-up, review comments need replies, and merge conflicts have to reach the right worker.
|
AI coding agents become much more useful when they can work in parallel, but parallel work gets messy quickly. Branches overlap, terminals get lost, CI failures need follow-up, review comments need replies, and merge conflicts have to reach the right worker.
|
||||||
|
|
@ -40,8 +36,6 @@ Agent Orchestrator is built to keep that loop visible and manageable. It helps y
|
||||||
- Route CI failures, review comments, and merge conflicts back to the right session
|
- Route CI failures, review comments, and merge conflicts back to the right session
|
||||||
- Use different agent CLIs through one common supervisor
|
- Use different agent CLIs through one common supervisor
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
At a high level, Agent Orchestrator follows a simple loop:
|
At a high level, Agent Orchestrator follows a simple loop:
|
||||||
|
|
@ -55,211 +49,114 @@ At a high level, Agent Orchestrator follows a simple loop:
|
||||||
|
|
||||||
The result is a local control layer for agentic coding: agents still do the coding, while Agent Orchestrator keeps their workspaces, status, terminals, and feedback loops organized.
|
The result is a local control layer for agentic coding: agents still do the coding, while Agent Orchestrator keeps their workspaces, status, terminals, and feedback loops organized.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
### Witness AO's Journey on X
|
|
||||||
|
|
||||||
<table border="1" style="border-collapse: collapse; width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px; text-align: center; border: 1px solid #ddd;">
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2026329204405723180"><img src="screenshots/first.png" alt="First" width="400"></a><br><br>
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2026329204405723180">Visit</a>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center; border: 1px solid #ddd;">
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2025986105485733945"><img src="screenshots/second.png" alt="Second" width="400"></a><br><br>
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2025986105485733945">Visit</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="padding: 10px; text-align: center; border: 1px solid #ddd;">
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2064157228400341312"><img src="screenshots/third.png" alt="Third" width="400"></a><br><br>
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2064157228400341312">Visit</a>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; text-align: center; border: 1px solid #ddd;">
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2024885035774738700?s=20"><img src="screenshots/image.png" alt="Fourth" width="400"></a><br><br>
|
|
||||||
<a href="https://x.com/agent_wrapper/status/2024885035774738700?s=20">Visit</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
[What is Agent Orchestrator?](#what-is-agent-orchestrator) • [Why Agent Orchestrator?](#why-agent-orchestrator) • [How it works](#how-it-works) • [Features](#features) • [Quick Start](#quick-start) • [Architecture](#architecture) • [Documentation](#documentation) • [Contributing](#contributing)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
| Feature | Description |
|
The desktop app is the main control surface: projects on the left, active sessions in the center, and the selected session's terminal, pull request state, review runs, and browser preview in the inspector.
|
||||||
| :----------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| **Agent-Agnostic Platform** | 23+ agent adapters including [Claude Code](https://code.claude.com/docs/en/overview), [OpenAI Codex](https://openai.com/), [Cursor](https://cursor.com/), [OpenCode](https://opencode.ai/), [Aider](https://aider.chat/), [Amp](https://ampcode.com/manual), [Goose](https://goose-docs.ai/), [GitHub Copilot](https://github.com/features/copilot), [Grok](https://x.ai/grok), [Qwen Code](https://github.com/QwenLM/qwen-code), [Kimi Code](https://www.kimi.com/code), [Cline](https://cline.bot/), [Continue](https://www.continue.dev/), [Kiro](https://kiro.dev/), and more |
|
|
||||||
| **Isolated Workspaces** | Each session spawns into its own git worktree with dedicated runtime |
|
|
||||||
| **Platform-Native Runtimes** | tmux on Darwin/Linux, conpty on Windows for optimal performance |
|
|
||||||
| **Live PR Observation** | Provider-neutral SCM observer with automatic feedback routing |
|
|
||||||
| **Automatic Feedback Routing** | CI failures, review comments, and merge conflicts routed to the owning agent |
|
|
||||||
| **Durable Facts Storage** | SQLite persists immutable facts with display status derived at read time |
|
|
||||||
| **CDC Broadcasting** | DB triggers append changes to change_log, broadcasted via SSE |
|
|
||||||
| **Desktop Experience** | Native Electron app with React UI and live terminal streaming |
|
|
||||||
| **Loopback-Only Daemon** | HTTP control over 127.0.0.1 with no auth, CORS, or TLS by design |
|
|
||||||
|
|
||||||
### Supported Agents
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="36%">
|
||||||
|
<h3>Parallel agent sessions</h3>
|
||||||
|
<p>Start multiple coding agents from the same project without mixing files, branches, terminals, or pull request state.</p>
|
||||||
|
</td>
|
||||||
|
<td width="64%">
|
||||||
|
<img src="docs/assets/readme/dashboard.png" alt="Agent Orchestrator board with multiple parallel sessions" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="36%">
|
||||||
|
<h3>Live terminal control</h3>
|
||||||
|
<p>Open any session and attach to the worker terminal while keeping session summary, PR state, and follow-up actions in view.</p>
|
||||||
|
</td>
|
||||||
|
<td width="64%">
|
||||||
|
<img src="docs/assets/readme/session-terminal.png" alt="Session terminal inside Agent Orchestrator" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="36%">
|
||||||
|
<h3>Review feedback loop</h3>
|
||||||
|
<p>Run reviewer agents, inspect review status, and route requested changes back to the right worker session.</p>
|
||||||
|
</td>
|
||||||
|
<td width="64%">
|
||||||
|
<img src="docs/assets/readme/reviews-tab.png" alt="Reviews tab showing reviewer runs and actions" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="36%">
|
||||||
|
<h3>In-app browser preview</h3>
|
||||||
|
<p>Preview a session's local app beside the terminal so UI work, browser state, and agent output stay together.</p>
|
||||||
|
</td>
|
||||||
|
<td width="64%">
|
||||||
|
<img src="docs/assets/readme/browser-preview.png" alt="Browser preview tab showing a local app preview" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
Works with 23+ CLI-based coding agents including Claude Code, OpenAI Codex, Cursor, OpenCode, Aider, Amp, Goose, GitHub Copilot, Grok, Qwen Code, Kimi Code, Crush, Cline, Droid, Devin, Auggie, Continue, Kiro, and Kilo Code.
|
## Supported Agents
|
||||||
|
|
||||||
For direct CLI usage, including agent readiness checks and context-aware session
|
AO ships adapters for 23 worker agent harnesses:
|
||||||
spawns, see the [CLI Guide](docs/cli/README.md).
|
|
||||||
|
`claude-code` · `codex` · `aider` · `opencode` · `grok` · `droid` · `amp` · `agy` · `crush` · `cursor` · `qwen` · `copilot` · `goose` · `auggie` · `continue` · `devin` · `cline` · `kimi` · `kiro` · `kilocode` · `vibe` · `pi` · `autohand`
|
||||||
|
|
||||||
|
Reviewer agents are configured separately. The current reviewer harnesses are:
|
||||||
|
|
||||||
|
`claude-code` · `codex` · `opencode`
|
||||||
|
|
||||||
**If it runs in a terminal, it runs on Agent Orchestrator.**
|
**If it runs in a terminal, it runs on Agent Orchestrator.**
|
||||||
|
|
||||||
---
|
## Download
|
||||||
|
|
||||||
## Quick Start
|
Download the latest desktop build for your platform:
|
||||||
|
|
||||||
### Prerequisites
|
| Platform | Download |
|
||||||
|
| -------- | ------------------------------------------------------------------------------------------------- |
|
||||||
|
| Windows | [Setup.exe](https://github.com/AgentWrapper/agent-orchestrator/releases/latest) |
|
||||||
|
| macOS | [Agent Orchestrator.dmg](https://github.com/AgentWrapper/agent-orchestrator/releases/latest) |
|
||||||
|
| Linux | [Agent Orchestrator.AppImage](https://github.com/AgentWrapper/agent-orchestrator/releases/latest) |
|
||||||
|
|
||||||
| Requirement | Minimum | Recommended |
|
## Witness AO's Journey on X
|
||||||
| ----------- | ------- | ----------- |
|
|
||||||
| Go | 1.25+ | Latest |
|
|
||||||
| Node.js | 20+ | Latest LTS |
|
|
||||||
| Git | Any | Latest |
|
|
||||||
| pnpm | Any | Latest |
|
|
||||||
|
|
||||||
**Optional:**
|
<table>
|
||||||
|
<tr>
|
||||||
- `tmux` (Darwin/Linux) - For Unix runtime
|
<td width="50%">
|
||||||
- `gh` (GitHub CLI) - For authenticated GitHub API calls
|
<a href="https://x.com/agent_wrapper/status/2026329204405723180">
|
||||||
|
<img src="screenshots/first.png" alt="Agent Orchestrator journey screenshot one" />
|
||||||
### Installation
|
</a>
|
||||||
|
</td>
|
||||||
Download the latest release for your platform:
|
<td width="50%">
|
||||||
|
<a href="https://x.com/agent_wrapper/status/2025986105485733945">
|
||||||
| Platform | Download |
|
<img src="screenshots/second.png" alt="Agent Orchestrator journey screenshot two" />
|
||||||
| ----------- | ------------------------------------------------------------------------------------------------- |
|
</a>
|
||||||
| **Windows** | [Setup.exe](https://github.com/AgentWrapper/agent-orchestrator/releases/latest) |
|
</td>
|
||||||
| **macOS** | [Agent Orchestrator.dmg](https://github.com/AgentWrapper/agent-orchestrator/releases/latest) |
|
</tr>
|
||||||
| **Linux** | [Agent Orchestrator.AppImage](https://github.com/AgentWrapper/agent-orchestrator/releases/latest) |
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
**Direct Download:** [Latest Release](https://github.com/AgentWrapper/agent-orchestrator/releases/latest)
|
<a href="https://x.com/agent_wrapper/status/2064157228400341312">
|
||||||
|
<img src="screenshots/third.png" alt="Agent Orchestrator journey screenshot three" />
|
||||||
---
|
</a>
|
||||||
|
</td>
|
||||||
## Telemetry
|
<td width="50%">
|
||||||
|
<a href="https://x.com/agent_wrapper/status/2024885035774738700?s=20">
|
||||||
Agent Orchestrator collects minimal telemetry for reliability and product understanding. Data is stored locally by default; remote transmission is opt-in via environment variables. [Read the full telemetry policy](docs/telemetry.md).
|
<img src="screenshots/image.png" alt="Agent Orchestrator journey screenshot four" />
|
||||||
|
</a>
|
||||||
---
|
</td>
|
||||||
|
</tr>
|
||||||
## Architecture
|
</table>
|
||||||
|
|
||||||
Agent Orchestrator is a long-running Go daemon built around **inbound/outbound port contracts** with swappable adapters.
|
|
||||||
|
|
||||||
**Core mental model:** OBSERVE external facts → UPDATE durable facts → DERIVE display status / ACT
|
|
||||||
|
|
||||||
**Key components:**
|
|
||||||
|
|
||||||
- **Frontend** - Electron + React UI with TanStack Router/Query and shadcn/ui
|
|
||||||
- **Backend Daemon** - Go-based HTTP server with controllers, services, and adapters
|
|
||||||
- **Runtime** - Platform-specific: `tmux` on Darwin/Linux, `conpty` on Windows
|
|
||||||
- **Storage** - SQLite with change-data-capture (CDC) for real-time updates
|
|
||||||
- **Adapters** - 23+ agent adapters, git worktree workspace, GitHub SCM integration
|
|
||||||
|
|
||||||
For detailed architecture diagrams, data flows, and load-bearing rules, see [architecture.md](docs/architecture.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
| Document | Description |
|
| Document | Start here when you need |
|
||||||
| -------------------------------------------------------- | ------------------------------------------------------- |
|
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||||
| [Architecture](docs/architecture.md) | System architecture, data flows, and load-bearing rules |
|
| [docs/architecture.md](docs/architecture.md) | Backend mental model, lifecycle, persistence, CDC, status derivation, and daemon boundaries. |
|
||||||
| [Backend Code Structure](docs/backend-code-structure.md) | Package-by-package ownership and dependency rules |
|
| [docs/backend-code-structure.md](docs/backend-code-structure.md) | Package ownership and where each backend concern belongs. |
|
||||||
| [CLI Guide](docs/cli/README.md) | Direct `ao` CLI usage, command routes, and smoke tests |
|
| [docs/cli/README.md](docs/cli/README.md) | CLI behavior and daemon route mapping. |
|
||||||
| [AGENTS.md](AGENTS.md) | Contributor and worker-agent contract |
|
| [docs/STATUS.md](docs/STATUS.md) | What currently ships on `main` and what remains in flight. |
|
||||||
|
| [docs/stack.md](docs/stack.md) | Library, runtime, and dependency decisions. |
|
||||||
|
|
||||||
---
|
## Telemetry
|
||||||
|
|
||||||
## Testing
|
Agent Orchestrator collects minimal telemetry for reliability and product understanding. Data is stored locally by default; remote transmission is opt-in through environment variables. See [docs/telemetry.md](docs/telemetry.md).
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend tests
|
|
||||||
cd backend
|
|
||||||
go test -race ./...
|
|
||||||
|
|
||||||
# Frontend tests
|
|
||||||
cd frontend
|
|
||||||
pnpm test
|
|
||||||
|
|
||||||
# Full CI validation locally
|
|
||||||
npx @redwoodjs/agent-ci run --all
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
All configuration is environment-driven. The daemon takes no config file.
|
|
||||||
|
|
||||||
| Variable | Default | Purpose |
|
|
||||||
| --------------------- | -------------------- | --------------------------- |
|
|
||||||
| `AO_PORT` | `3001` | HTTP bind port |
|
|
||||||
| `AO_REQUEST_TIMEOUT` | `60s` | Per-request timeout |
|
|
||||||
| `AO_SHUTDOWN_TIMEOUT` | `10s` | Graceful shutdown cap |
|
|
||||||
| `AO_RUN_FILE` | `~/.ao/running.json` | PID/port handshake |
|
|
||||||
| `AO_DATA_DIR` | `~/.ao/data` | SQLite data directory |
|
|
||||||
| `AO_AGENT` | `claude-code` | Compatibility agent adapter |
|
|
||||||
| `GITHUB_TOKEN` | - | GitHub auth token |
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl localhost:3001/healthz # Liveness probe
|
|
||||||
curl localhost:3001/readyz # Readiness probe
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
We love contributions! Join our community on Discord to get started.
|
|
||||||
|
|
||||||
### Join us on Discord
|
|
||||||
|
|
||||||
[](https://discord.com/invite/UZv7JjxbwG)
|
|
||||||
|
|
||||||
**Daily contributor sync:** Every day at **10:00 PM IST**
|
|
||||||
|
|
||||||
Get your issues verified by core contributors, ask questions, share progress, and learn from the community. New contributors are always welcome!
|
|
||||||
|
|
||||||
**Why join Discord?**
|
|
||||||
|
|
||||||
- Get your issues and PRs verified by core contributors before investing time
|
|
||||||
- Learn from experienced contributors in daily sync calls
|
|
||||||
- Share your progress and get feedback
|
|
||||||
- Get help troubleshooting in real-time
|
|
||||||
- Stay updated on the latest developments and roadmap
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
1. **Join the Discord** - Connect with the community and get guidance
|
|
||||||
2. **Read the contributor contract** - See [AGENTS.md](AGENTS.md) for repo layout, daemon/API boundaries, and coding conventions
|
|
||||||
3. **Pick a focused problem** - Browse [open issues](https://github.com/AgentWrapper/agent-orchestrator/issues) and choose one small enough for a focused PR
|
|
||||||
4. **Open a clear PR** - Keep changes narrow, explain user-visible impact, link issues, include tests
|
|
||||||
5. **Iterate with contributors** - Use review feedback to tighten the PR until verified
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache License 2.0 - see [LICENSE](LICENSE) for details.
|
Apache License 2.0. See [LICENSE](LICENSE).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
**[Star us on GitHub](https://github.com/AgentWrapper/agent-orchestrator)** • **[Report Issues](https://github.com/AgentWrapper/agent-orchestrator/issues)** • **[Discussions](https://github.com/AgentWrapper/agent-orchestrator/discussions)**
|
|
||||||
|
|
||||||
Made with love by the Agent Orchestrator community
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -38,6 +38,7 @@ export function BrowserPanelView({
|
||||||
}: BrowserPanelProps & { browserView: BrowserViewModel }) {
|
}: BrowserPanelProps & { browserView: BrowserViewModel }) {
|
||||||
const { navState, slotRef, navigate, goBack, goForward, reload, stop } = browserView;
|
const { navState, slotRef, navigate, goBack, goForward, reload, stop } = browserView;
|
||||||
const [urlInput, setUrlInput] = useState(navState.url);
|
const [urlInput, setUrlInput] = useState(navState.url);
|
||||||
|
const showStaticPreview = !window.ao?.browser && navState.url !== "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrlInput(navState.url);
|
setUrlInput(navState.url);
|
||||||
|
|
@ -111,6 +112,7 @@ export function BrowserPanelView({
|
||||||
</form>
|
</form>
|
||||||
<div className="browser-panel__content">
|
<div className="browser-panel__content">
|
||||||
<div className="browser-panel__slot" ref={slotRef} />
|
<div className="browser-panel__slot" ref={slotRef} />
|
||||||
|
{showStaticPreview ? <StaticPreview url={navState.url} /> : null}
|
||||||
{navState.url === "" ? (
|
{navState.url === "" ? (
|
||||||
<div className="browser-panel__overlay">
|
<div className="browser-panel__overlay">
|
||||||
<p>Enter a dev-server URL to preview it here.</p>
|
<p>Enter a dev-server URL to preview it here.</p>
|
||||||
|
|
@ -121,3 +123,48 @@ export function BrowserPanelView({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StaticPreview({ url }: { url: string }) {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 overflow-auto bg-[#f7f8fb] text-[#17202a]">
|
||||||
|
<div className="border-b border-[#dfe4ea] bg-white px-4 py-3">
|
||||||
|
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[#687384]">AO Preview</div>
|
||||||
|
<div className="mt-1 truncate font-mono text-[12px] text-[#2f5b9d]">{url}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto max-w-[760px] px-5 py-6">
|
||||||
|
<div className="rounded-[8px] border border-[#d7dee8] bg-white p-5 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-[22px] font-semibold leading-tight tracking-normal text-[#111827]">
|
||||||
|
Demo app preview
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 text-[13px] leading-5 text-[#526070]">
|
||||||
|
The worker exposed a local Vite app with <span className="font-mono">ao preview</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-[6px] bg-[#e7f8ed] px-2.5 py-1 text-[11px] font-semibold text-[#177245]">
|
||||||
|
Loaded
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 grid grid-cols-3 gap-3">
|
||||||
|
{[
|
||||||
|
["Routes", "12 passing"],
|
||||||
|
["Build", "ready"],
|
||||||
|
["Latency", "42 ms"],
|
||||||
|
].map(([label, value]) => (
|
||||||
|
<div key={label} className="rounded-[7px] border border-[#e1e7ef] bg-[#fbfcfe] p-3">
|
||||||
|
<div className="text-[11px] font-medium uppercase tracking-[0.06em] text-[#687384]">{label}</div>
|
||||||
|
<div className="mt-1 text-[15px] font-semibold text-[#111827]">{value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 rounded-[7px] border border-[#dce4ef] bg-[#0f172a] p-3 font-mono text-[12px] leading-5 text-[#cbd5e1]">
|
||||||
|
<div>$ npm run dev -- --host 127.0.0.1</div>
|
||||||
|
<div className="text-[#86efac]">ready in 418 ms</div>
|
||||||
|
<div>Local: http://localhost:5173/</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,18 @@ export function OrchestratorReplacementDialog({
|
||||||
<AlertTriangle className="size-4" aria-hidden="true" />
|
<AlertTriangle className="size-4" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<Dialog.Title className="text-sm font-medium text-foreground">Orchestrator replacement failed</Dialog.Title>
|
<Dialog.Title className="text-sm font-medium text-foreground">
|
||||||
|
Orchestrator replacement failed
|
||||||
|
</Dialog.Title>
|
||||||
<Dialog.Description className="mt-2 text-[13px] leading-5 text-muted-foreground">
|
<Dialog.Description className="mt-2 text-[13px] leading-5 text-muted-foreground">
|
||||||
{error ?? "The project orchestrator could not be replaced."}
|
{error ?? "The project orchestrator could not be replaced."}
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
<button className="rounded-md p-1 text-passive hover:bg-interactive-hover hover:text-foreground" type="button">
|
<button
|
||||||
|
className="rounded-md p-1 text-passive hover:bg-interactive-hover hover:text-foreground"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
<X className="size-4" aria-hidden="true" />
|
<X className="size-4" aria-hidden="true" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,11 @@ beforeEach(() => {
|
||||||
putMock.mockReset();
|
putMock.mockReset();
|
||||||
postMock.mockReset();
|
postMock.mockReset();
|
||||||
putMock.mockResolvedValue({ data: { project: {} }, error: undefined });
|
putMock.mockResolvedValue({ data: { project: {} }, error: undefined });
|
||||||
postMock.mockResolvedValue({ data: { orchestrator: { id: "proj-1-orch-2" } }, error: undefined, response: { status: 200 } });
|
postMock.mockResolvedValue({
|
||||||
|
data: { orchestrator: { id: "proj-1-orch-2" } },
|
||||||
|
error: undefined,
|
||||||
|
response: { status: 200 },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ProjectSettingsForm", () => {
|
describe("ProjectSettingsForm", () => {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ const VIEWS: { id: InspectorView; label: string; icon: ReactNode }[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const usePreviewData = import.meta.env.VITE_NO_ELECTRON === "1";
|
||||||
|
|
||||||
const prStateTone: Record<SessionPRSummary["state"], string> = {
|
const prStateTone: Record<SessionPRSummary["state"], string> = {
|
||||||
open: "border-success/40 bg-success/10 text-success",
|
open: "border-success/40 bg-success/10 text-success",
|
||||||
draft: "border-border bg-raised text-muted-foreground",
|
draft: "border-border bg-raised text-muted-foreground",
|
||||||
|
|
@ -442,6 +444,7 @@ function ReviewsView({
|
||||||
return reviews.some((review) => review.status === "running") ? 2500 : false;
|
return reviews.some((review) => review.status === "running") ? 2500 : false;
|
||||||
},
|
},
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if (usePreviewData) return mockReviewsResponse(session);
|
||||||
const { data, error } = await apiClient.GET("/api/v1/sessions/{sessionId}/reviews", {
|
const { data, error } = await apiClient.GET("/api/v1/sessions/{sessionId}/reviews", {
|
||||||
params: { path: { sessionId: session.id } },
|
params: { path: { sessionId: session.id } },
|
||||||
});
|
});
|
||||||
|
|
@ -453,6 +456,7 @@ function ReviewsView({
|
||||||
queryKey: ["project-config", session.workspaceId],
|
queryKey: ["project-config", session.workspaceId],
|
||||||
enabled: hasPr,
|
enabled: hasPr,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
if (usePreviewData) return mockProjectConfig();
|
||||||
const { data, error } = await apiClient.GET("/api/v1/projects/{id}", {
|
const { data, error } = await apiClient.GET("/api/v1/projects/{id}", {
|
||||||
params: { path: { id: session.workspaceId } },
|
params: { path: { id: session.workspaceId } },
|
||||||
});
|
});
|
||||||
|
|
@ -512,6 +516,76 @@ function projectConfig(project: components["schemas"]["ProjectOrDegraded"] | und
|
||||||
return project.config;
|
return project.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockProjectConfig(): ProjectConfig {
|
||||||
|
return {
|
||||||
|
worker: { agent: "codex" },
|
||||||
|
orchestrator: { agent: "codex" },
|
||||||
|
reviewers: [{ harness: "codex" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockReviewsResponse(session: WorkspaceSession): ReviewsResponse {
|
||||||
|
return {
|
||||||
|
reviewerHandleId: `${session.id}-reviewer`,
|
||||||
|
reviews: sortedPRs(session).map((pr, index) => {
|
||||||
|
const targetSha = `demo${pr.number}${index}`;
|
||||||
|
const reviewedAt = new Date(Date.now() - (index + 1) * 11 * 60 * 1000).toISOString();
|
||||||
|
const latestRun =
|
||||||
|
pr.review === "approved" || pr.review === "changes_requested"
|
||||||
|
? {
|
||||||
|
batchId: `demo-batch-${session.id}`,
|
||||||
|
body:
|
||||||
|
pr.review === "approved"
|
||||||
|
? "Demo review approved. The implementation is ready for the README screenshot flow."
|
||||||
|
: "Demo review found polish feedback for the terminal presentation.",
|
||||||
|
createdAt: reviewedAt,
|
||||||
|
githubReviewId: `${pr.number}01`,
|
||||||
|
harness: "codex",
|
||||||
|
id: `demo-review-run-${pr.number}`,
|
||||||
|
prUrl: pr.url,
|
||||||
|
reviewId: `demo-review-${pr.number}`,
|
||||||
|
sessionId: session.id,
|
||||||
|
status: "delivered",
|
||||||
|
targetSha,
|
||||||
|
verdict: pr.review === "approved" ? "approved" : "changes_requested",
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
latestRun,
|
||||||
|
prNumber: pr.number,
|
||||||
|
prUrl: pr.url,
|
||||||
|
status:
|
||||||
|
pr.review === "approved"
|
||||||
|
? "up_to_date"
|
||||||
|
: pr.review === "changes_requested"
|
||||||
|
? "changes_requested"
|
||||||
|
: pr.state === "draft"
|
||||||
|
? "ineligible"
|
||||||
|
: "needs_review",
|
||||||
|
targetSha,
|
||||||
|
title: mockReviewTitle(pr.number),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockReviewTitle(prNumber: number): string {
|
||||||
|
switch (prNumber) {
|
||||||
|
case 319:
|
||||||
|
return "Browser preview rail renders inside AO";
|
||||||
|
case 320:
|
||||||
|
return "Review tab keeps stacked PR rows visible";
|
||||||
|
case 321:
|
||||||
|
return "Draft child PR waits for parent review";
|
||||||
|
case 318:
|
||||||
|
return "Terminal polish feedback";
|
||||||
|
case 323:
|
||||||
|
return "README screenshot assets ready";
|
||||||
|
default:
|
||||||
|
return `Demo pull request ${prNumber}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ReviewPanel({
|
function ReviewPanel({
|
||||||
session,
|
session,
|
||||||
config,
|
config,
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,13 @@ export function SessionsBoard({ projectId }: SessionsBoardProps) {
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<OrchestratorIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
<OrchestratorIcon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
{isProjectRestarting ? "Restarting..." : isSpawning ? "Spawning..." : orchestrator ? "Orchestrator" : "Spawn Orchestrator"}
|
{isProjectRestarting
|
||||||
|
? "Restarting..."
|
||||||
|
: isSpawning
|
||||||
|
? "Spawning..."
|
||||||
|
: orchestrator
|
||||||
|
? "Orchestrator"
|
||||||
|
: "Spawn Orchestrator"}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
|
||||||
|
|
@ -557,7 +557,13 @@ function ProjectItem({
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{isProjectRestarting ? "Restarting…" : isSpawning ? "Spawning…" : orchestrator ? "Orchestrator" : "Spawn orchestrator"}
|
{isProjectRestarting
|
||||||
|
? "Restarting…"
|
||||||
|
: isSpawning
|
||||||
|
? "Spawning…"
|
||||||
|
: orchestrator
|
||||||
|
? "Orchestrator"
|
||||||
|
: "Spawn orchestrator"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ export function TerminalPane({ session, theme, daemonReady, terminalTarget, font
|
||||||
|
|
||||||
if (!window.ao) {
|
if (!window.ao) {
|
||||||
const provider = terminalTarget?.kind === "reviewer" ? terminalTarget.harness : (session?.provider ?? "claude");
|
const provider = terminalTarget?.kind === "reviewer" ? terminalTarget.harness : (session?.provider ?? "claude");
|
||||||
|
const lines =
|
||||||
|
terminalTarget?.kind === "reviewer" ? reviewerPreviewLines(session) : workerPreviewLines(session, provider);
|
||||||
return (
|
return (
|
||||||
<pre
|
<pre
|
||||||
className="h-full overflow-auto bg-terminal p-4 font-mono leading-relaxed text-[var(--term-fg)]"
|
className="h-full overflow-auto bg-terminal p-4 font-mono leading-relaxed text-[var(--term-fg)]"
|
||||||
|
|
@ -31,11 +33,23 @@ export function TerminalPane({ session, theme, daemonReady, terminalTarget, font
|
||||||
<span className="text-[var(--term-dim)]">~/{session?.workspaceName ?? "reverbcode"}</span>{" "}
|
<span className="text-[var(--term-dim)]">~/{session?.workspaceName ?? "reverbcode"}</span>{" "}
|
||||||
<span className="text-[var(--term-blue)]">{session?.branch || "main"}</span> $ {provider}
|
<span className="text-[var(--term-blue)]">{session?.branch || "main"}</span> $ {provider}
|
||||||
{"\n"}
|
{"\n"}
|
||||||
<span className="text-[var(--term-green)]">✻ Welcome to the agent CLI</span>
|
{lines.map((line, index) => (
|
||||||
{"\n\n"}
|
<span
|
||||||
<span className="text-[var(--term-dim)]">
|
key={`${line}:${index}`}
|
||||||
Browser preview renders a static terminal surface. Electron attaches the live PTY.
|
className={
|
||||||
</span>
|
line.startsWith("PASS") || line.startsWith("DONE")
|
||||||
|
? "text-[var(--term-green)]"
|
||||||
|
: line.startsWith("WARN") || line.startsWith("TODO")
|
||||||
|
? "text-[var(--term-amber)]"
|
||||||
|
: line.startsWith("$")
|
||||||
|
? "text-[var(--term-blue)]"
|
||||||
|
: "text-[var(--term-fg)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
{"\n"}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +66,57 @@ export function TerminalPane({ session, theme, daemonReady, terminalTarget, font
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function workerPreviewLines(session: WorkspaceSession | undefined, provider: string): string[] {
|
||||||
|
if (session?.id === "demo-review-stack") {
|
||||||
|
return [
|
||||||
|
'$ rg "previewUrl|Browser" frontend/src/renderer',
|
||||||
|
"frontend/src/renderer/components/SessionInspector.tsx: Browser tab selected after ao preview",
|
||||||
|
"frontend/src/renderer/hooks/useBrowserView.ts: preview revision re-navigates the view",
|
||||||
|
"$ ao preview http://localhost:5173",
|
||||||
|
"DONE preview target set for demo-review-stack",
|
||||||
|
"$ npm --prefix frontend run typecheck",
|
||||||
|
"PASS TypeScript project references are clean",
|
||||||
|
"TODO wait for reviewer on PR #320 before merging the stack",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (session?.id === "demo-working") {
|
||||||
|
return [
|
||||||
|
`$ ${provider} --continue`,
|
||||||
|
"Reading renderer board and inspector components...",
|
||||||
|
"Updated demo workspace data for README screenshots",
|
||||||
|
"$ npm --prefix frontend test -- SessionsBoard SessionInspector",
|
||||||
|
"PASS 18 tests passed",
|
||||||
|
"DONE board has Working, Needs you, In review, and Ready to merge populated",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (session?.id === "demo-needs-input") {
|
||||||
|
return [
|
||||||
|
"$ git diff --stat",
|
||||||
|
"frontend/src/renderer/components/TerminalPane.tsx | 41 +++++++++++++++++",
|
||||||
|
"frontend/src/renderer/styles.css | 27 +++++++++++",
|
||||||
|
"WARN reviewer requested a tighter terminal activity sample",
|
||||||
|
"TODO confirm whether to keep the toolbar density change",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`$ ${provider} --status`,
|
||||||
|
"Reading task context and local diff...",
|
||||||
|
"Running focused validation for the current session",
|
||||||
|
"PASS demo terminal is populated for screenshots",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function reviewerPreviewLines(session: WorkspaceSession | undefined): string[] {
|
||||||
|
return [
|
||||||
|
"$ ao review submit --session " + (session?.id ?? "demo-session"),
|
||||||
|
"Reviewing PR #319: browser preview rail renders inside AO",
|
||||||
|
"PASS implementation matches the requested README screenshot flow",
|
||||||
|
"Reviewing PR #320: stacked PR review rows",
|
||||||
|
"WARN keep multiple review rows visible before taking the screenshot",
|
||||||
|
"DONE submitted batched review results",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function bannerText(state: TerminalSessionState, error?: string): string | undefined {
|
function bannerText(state: TerminalSessionState, error?: string): string | undefined {
|
||||||
if (state === "reattaching") return "Terminal disconnected — reattaching…";
|
if (state === "reattaching") return "Terminal disconnected — reattaching…";
|
||||||
if (state === "error") return `Terminal error: ${error ?? "connection failed"}`;
|
if (state === "error") return `Terminal error: ${error ?? "connection failed"}`;
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ export function useBrowserView({
|
||||||
const observerRef = useRef<ResizeObserver | null>(null);
|
const observerRef = useRef<ResizeObserver | null>(null);
|
||||||
const previewTriggerRef = useRef<{ revision: number | null; target: string } | null>(null);
|
const previewTriggerRef = useRef<{ revision: number | null; target: string } | null>(null);
|
||||||
const hasUrlRef = useRef(false);
|
const hasUrlRef = useRef(false);
|
||||||
|
const hasNativeBrowser = Boolean(window.ao?.browser);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activeRef.current = active;
|
activeRef.current = active;
|
||||||
|
|
@ -176,6 +177,21 @@ export function useBrowserView({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
if (!hasNativeBrowser) {
|
||||||
|
const state = {
|
||||||
|
...EMPTY_NAV_STATE,
|
||||||
|
viewId: `preview-${sessionId}`,
|
||||||
|
url: "",
|
||||||
|
title: "",
|
||||||
|
};
|
||||||
|
viewIdRef.current = state.viewId;
|
||||||
|
setViewId(state.viewId);
|
||||||
|
setNavState(state);
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
viewIdRef.current = "";
|
||||||
|
};
|
||||||
|
}
|
||||||
window.ao?.browser.ensure(sessionId).then((state) => {
|
window.ao?.browser.ensure(sessionId).then((state) => {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
viewIdRef.current = state.viewId;
|
viewIdRef.current = state.viewId;
|
||||||
|
|
@ -191,7 +207,7 @@ export function useBrowserView({
|
||||||
}
|
}
|
||||||
viewIdRef.current = "";
|
viewIdRef.current = "";
|
||||||
};
|
};
|
||||||
}, [scheduleSettleMeasure, sendHiddenBounds, sessionId]);
|
}, [hasNativeBrowser, scheduleSettleMeasure, sendHiddenBounds, sessionId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return window.ao?.browser.onNavState((state) => {
|
return window.ao?.browser.onNavState((state) => {
|
||||||
|
|
@ -229,11 +245,29 @@ export function useBrowserView({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const navigate = useCallback(
|
const navigate = useCallback(
|
||||||
(url: string) => withView((id) => window.ao!.browser.navigate({ viewId: id, url })),
|
(url: string) => {
|
||||||
[withView],
|
if (!hasNativeBrowser) {
|
||||||
|
const normalized = url.trim();
|
||||||
|
setNavState((current) => ({
|
||||||
|
...current,
|
||||||
|
url: normalized,
|
||||||
|
title: normalized ? "AO preview" : "",
|
||||||
|
isLoading: false,
|
||||||
|
}));
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return withView((id) => window.ao!.browser.navigate({ viewId: id, url }));
|
||||||
|
},
|
||||||
|
[hasNativeBrowser, withView],
|
||||||
);
|
);
|
||||||
|
|
||||||
const clear = useCallback(() => withView((id) => window.ao!.browser.clear(id)), [withView]);
|
const clear = useCallback(() => {
|
||||||
|
if (!hasNativeBrowser) {
|
||||||
|
setNavState((current) => ({ ...current, url: "", title: "", isLoading: false }));
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return withView((id) => window.ao!.browser.clear(id));
|
||||||
|
}, [hasNativeBrowser, withView]);
|
||||||
|
|
||||||
// When the session is terminated, clear the view and stop reacting to
|
// When the session is terminated, clear the view and stop reacting to
|
||||||
// daemon-driven preview changes so stale content does not remain visible.
|
// daemon-driven preview changes so stale content does not remain visible.
|
||||||
|
|
@ -273,10 +307,10 @@ export function useBrowserView({
|
||||||
navState,
|
navState,
|
||||||
slotRef,
|
slotRef,
|
||||||
navigate,
|
navigate,
|
||||||
goBack: () => withView((id) => window.ao!.browser.goBack(id)),
|
goBack: () => (hasNativeBrowser ? withView((id) => window.ao!.browser.goBack(id)) : Promise.resolve()),
|
||||||
goForward: () => withView((id) => window.ao!.browser.goForward(id)),
|
goForward: () => (hasNativeBrowser ? withView((id) => window.ao!.browser.goForward(id)) : Promise.resolve()),
|
||||||
reload: () => withView((id) => window.ao!.browser.reload(id)),
|
reload: () => (hasNativeBrowser ? withView((id) => window.ao!.browser.reload(id)) : Promise.resolve()),
|
||||||
stop: () => withView((id) => window.ao!.browser.stop(id)),
|
stop: () => (hasNativeBrowser ? withView((id) => window.ao!.browser.stop(id)) : Promise.resolve()),
|
||||||
destroy,
|
destroy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,12 @@ describe("useWorkspaceQuery", () => {
|
||||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||||
|
|
||||||
const [workspace] = result.current.data ?? [];
|
const [workspace] = result.current.data ?? [];
|
||||||
expect(workspace).toMatchObject({ id: "proj-1", name: "my-app", path: "/home/me/my-app", orchestratorAgent: "codex" });
|
expect(workspace).toMatchObject({
|
||||||
|
id: "proj-1",
|
||||||
|
name: "my-app",
|
||||||
|
path: "/home/me/my-app",
|
||||||
|
orchestratorAgent: "codex",
|
||||||
|
});
|
||||||
expect(workspace.sessions).toHaveLength(2);
|
expect(workspace.sessions).toHaveLength(2);
|
||||||
expect(workspace.sessions[0]).toMatchObject({
|
expect(workspace.sessions[0]).toMatchObject({
|
||||||
id: "sess-1",
|
id: "sess-1",
|
||||||
|
|
|
||||||
|
|
@ -1,285 +1,203 @@
|
||||||
import type { PRState, PullRequestFacts, WorkspaceSummary } from "../types/workspace";
|
import type { PRState, PullRequestFacts, WorkspaceSummary } from "../types/workspace";
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
const minutesAgo = (minutes: number) => new Date(Date.now() - minutes * 60 * 1000).toISOString();
|
||||||
const hoursAgo = (hours: number) => new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
const hoursAgo = (hours: number) => new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
||||||
|
|
||||||
// Single-PR preview helper. Sessions that own a stack (stacked-auth) inline
|
const demoPr = (
|
||||||
// their facts instead; the daemon aggregates per-PR CI/review server-side.
|
number: number,
|
||||||
const pr = (number: number, state: PRState, ci = "passing"): PullRequestFacts => ({
|
state: PRState,
|
||||||
url: `https://github.com/me/pull/${number}`,
|
ci: PullRequestFacts["ci"] = "passing",
|
||||||
|
review: PullRequestFacts["review"] = "none",
|
||||||
|
mergeability: PullRequestFacts["mergeability"] = "mergeable",
|
||||||
|
): PullRequestFacts => ({
|
||||||
|
url: `https://github.com/acme-inc/ao-demo/pull/${number}`,
|
||||||
number,
|
number,
|
||||||
state,
|
state,
|
||||||
ci,
|
ci,
|
||||||
review: state === "merged" ? "approved" : "none",
|
review,
|
||||||
mergeability: "mergeable",
|
mergeability,
|
||||||
reviewComments: false,
|
reviewComments: review === "changes_requested",
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mockWorkspaces: WorkspaceSummary[] = [
|
export const mockWorkspaces: WorkspaceSummary[] = [
|
||||||
{
|
{
|
||||||
id: "api-gateway",
|
id: "ao-demo",
|
||||||
name: "api-gateway",
|
name: "ao-demo",
|
||||||
path: "/Users/me/api-gateway",
|
path: "/demo/ao-demo",
|
||||||
|
type: "main",
|
||||||
|
orchestratorAgent: "codex",
|
||||||
|
accentColor: "#6ee7b7",
|
||||||
sessions: [
|
sessions: [
|
||||||
{
|
{
|
||||||
id: "refactor-mux",
|
id: "ao-demo-orchestrator",
|
||||||
terminalHandleId: "refactor-mux/terminal_0",
|
terminalHandleId: "ao-demo-orchestrator/terminal_0",
|
||||||
workspaceId: "api-gateway",
|
workspaceId: "ao-demo",
|
||||||
workspaceName: "api-gateway",
|
workspaceName: "ao-demo",
|
||||||
title: "Split terminal mux responsibilities",
|
title: "Project orchestrator",
|
||||||
provider: "claude-code",
|
|
||||||
branch: "feat/refactor-mux",
|
|
||||||
status: "working",
|
|
||||||
updatedAt: now,
|
|
||||||
createdAt: hoursAgo(4),
|
|
||||||
changedFiles: [
|
|
||||||
{
|
|
||||||
path: "internal/mux/terminal_mux.go",
|
|
||||||
additions: 42,
|
|
||||||
deletions: 8,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
commitMessage: "refactor terminal mux",
|
|
||||||
prs: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "stacked-auth",
|
|
||||||
terminalHandleId: "stacked-auth/terminal_0",
|
|
||||||
workspaceId: "api-gateway",
|
|
||||||
workspaceName: "api-gateway",
|
|
||||||
title: "auth stack",
|
|
||||||
provider: "claude-code",
|
|
||||||
branch: "feat/ns",
|
|
||||||
status: "review_pending",
|
|
||||||
updatedAt: now,
|
|
||||||
createdAt: hoursAgo(2),
|
|
||||||
// One session owning a stack: open root, draft child, merged base.
|
|
||||||
prs: [
|
|
||||||
{
|
|
||||||
url: "https://github.com/me/api-gateway/pull/41",
|
|
||||||
number: 41,
|
|
||||||
state: "open",
|
|
||||||
ci: "passing",
|
|
||||||
review: "approved",
|
|
||||||
mergeability: "mergeable",
|
|
||||||
reviewComments: false,
|
|
||||||
updatedAt: now,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "https://github.com/me/api-gateway/pull/42",
|
|
||||||
number: 42,
|
|
||||||
state: "draft",
|
|
||||||
ci: "pending",
|
|
||||||
review: "none",
|
|
||||||
mergeability: "unknown",
|
|
||||||
reviewComments: false,
|
|
||||||
updatedAt: now,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "https://github.com/me/api-gateway/pull/40",
|
|
||||||
number: 40,
|
|
||||||
state: "merged",
|
|
||||||
ci: "passing",
|
|
||||||
review: "approved",
|
|
||||||
mergeability: "mergeable",
|
|
||||||
reviewComments: false,
|
|
||||||
updatedAt: hoursAgo(1),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "fix-auth-timeouts",
|
|
||||||
workspaceId: "api-gateway",
|
|
||||||
workspaceName: "api-gateway",
|
|
||||||
title: "fix auth timeout retry loop",
|
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
branch: "fix/auth-timeouts",
|
kind: "orchestrator",
|
||||||
status: "ci_failed",
|
branch: "main",
|
||||||
updatedAt: hoursAgo(1),
|
status: "working",
|
||||||
createdAt: hoursAgo(6),
|
createdAt: hoursAgo(6),
|
||||||
prs: [pr(184, "open", "failing")],
|
updatedAt: minutesAgo(3),
|
||||||
},
|
activity: { state: "active", lastActivityAt: minutesAgo(3) },
|
||||||
{
|
|
||||||
id: "rate-limit-headers",
|
|
||||||
workspaceId: "api-gateway",
|
|
||||||
workspaceName: "api-gateway",
|
|
||||||
title: "add rate limit headers",
|
|
||||||
provider: "opencode",
|
|
||||||
branch: "feat/rate-limit-headers",
|
|
||||||
status: "review_pending",
|
|
||||||
updatedAt: hoursAgo(2),
|
|
||||||
createdAt: hoursAgo(9),
|
|
||||||
prs: [pr(185, "open")],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "webgl-preview",
|
|
||||||
name: "webgl-preview",
|
|
||||||
path: "/Users/me/webgl-preview",
|
|
||||||
sessions: [
|
|
||||||
{
|
|
||||||
id: "fix-webgl-fallback",
|
|
||||||
workspaceId: "webgl-preview",
|
|
||||||
workspaceName: "webgl-preview",
|
|
||||||
title: "Restore fallback renderer after WebGL init fails",
|
|
||||||
provider: "codex",
|
|
||||||
branch: "fix/webgl-fallback",
|
|
||||||
status: "needs_input",
|
|
||||||
updatedAt: now,
|
|
||||||
createdAt: hoursAgo(4),
|
|
||||||
prs: [],
|
prs: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "shader-cache",
|
id: "demo-working",
|
||||||
workspaceId: "webgl-preview",
|
terminalHandleId: "demo-working/terminal_0",
|
||||||
workspaceName: "webgl-preview",
|
workspaceId: "ao-demo",
|
||||||
title: "cache compiled shader programs",
|
workspaceName: "ao-demo",
|
||||||
provider: "claude-code",
|
title: "Build screenshot-ready dashboard data",
|
||||||
branch: "feat/shader-cache",
|
provider: "codex",
|
||||||
|
branch: "demo/dashboard-screenshot",
|
||||||
status: "working",
|
status: "working",
|
||||||
updatedAt: hoursAgo(0.5),
|
displayStatus: "working",
|
||||||
createdAt: hoursAgo(2),
|
|
||||||
changedFiles: [
|
|
||||||
{ path: "src/render/shader-cache.ts", additions: 86, deletions: 12 },
|
|
||||||
{ path: "src/render/webgl-context.ts", additions: 24, deletions: 5 },
|
|
||||||
],
|
|
||||||
prs: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "texture-leak",
|
|
||||||
workspaceId: "webgl-preview",
|
|
||||||
workspaceName: "webgl-preview",
|
|
||||||
title: "stop texture leak on scene reload",
|
|
||||||
provider: "codex",
|
|
||||||
branch: "fix/texture-leak",
|
|
||||||
status: "ci_failed",
|
|
||||||
updatedAt: hoursAgo(1.5),
|
|
||||||
createdAt: hoursAgo(7),
|
|
||||||
prs: [pr(51, "open", "failing")],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "review-camera-pan",
|
|
||||||
workspaceId: "webgl-preview",
|
|
||||||
workspaceName: "webgl-preview",
|
|
||||||
title: "smooth camera pan controls",
|
|
||||||
provider: "aider",
|
|
||||||
branch: "feat/camera-pan",
|
|
||||||
status: "review_pending",
|
|
||||||
updatedAt: hoursAgo(3),
|
|
||||||
createdAt: hoursAgo(10),
|
|
||||||
prs: [pr(52, "open")],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "draft-webgpu-probe",
|
|
||||||
workspaceId: "webgl-preview",
|
|
||||||
workspaceName: "webgl-preview",
|
|
||||||
title: "probe WebGPU support before init",
|
|
||||||
provider: "opencode",
|
|
||||||
branch: "feat/webgpu-probe",
|
|
||||||
status: "draft",
|
|
||||||
updatedAt: hoursAgo(5),
|
|
||||||
createdAt: hoursAgo(12),
|
|
||||||
prs: [pr(53, "draft")],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "merge-frame-stats",
|
|
||||||
workspaceId: "webgl-preview",
|
|
||||||
workspaceName: "webgl-preview",
|
|
||||||
title: "ship frame statistics overlay",
|
|
||||||
provider: "codex",
|
|
||||||
branch: "feat/frame-stats",
|
|
||||||
status: "mergeable",
|
|
||||||
updatedAt: hoursAgo(0.25),
|
|
||||||
createdAt: hoursAgo(14),
|
|
||||||
prs: [pr(54, "open")],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "approved-pixel-ratio",
|
|
||||||
workspaceId: "webgl-preview",
|
|
||||||
workspaceName: "webgl-preview",
|
|
||||||
title: "respect device pixel ratio",
|
|
||||||
provider: "claude-code",
|
|
||||||
branch: "fix/device-pixel-ratio",
|
|
||||||
status: "approved",
|
|
||||||
updatedAt: hoursAgo(2.5),
|
|
||||||
createdAt: hoursAgo(16),
|
|
||||||
prs: [pr(55, "open")],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "input-pointer-lock",
|
|
||||||
workspaceId: "webgl-preview",
|
|
||||||
workspaceName: "webgl-preview",
|
|
||||||
title: "pointer lock escape handling",
|
|
||||||
provider: "codex",
|
|
||||||
branch: "fix/pointer-lock",
|
|
||||||
status: "changes_requested",
|
|
||||||
updatedAt: hoursAgo(4),
|
|
||||||
createdAt: hoursAgo(18),
|
|
||||||
prs: [pr(56, "open")],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "mobile-shell",
|
|
||||||
name: "mobile-shell",
|
|
||||||
path: "/Users/me/mobile-shell",
|
|
||||||
sessions: [
|
|
||||||
{
|
|
||||||
id: "nav-gesture",
|
|
||||||
workspaceId: "mobile-shell",
|
|
||||||
workspaceName: "mobile-shell",
|
|
||||||
title: "repair back swipe gesture",
|
|
||||||
provider: "codex",
|
|
||||||
branch: "fix/back-swipe",
|
|
||||||
status: "working",
|
|
||||||
updatedAt: hoursAgo(0.75),
|
|
||||||
createdAt: hoursAgo(3),
|
createdAt: hoursAgo(3),
|
||||||
|
updatedAt: minutesAgo(2),
|
||||||
|
activity: { state: "active", lastActivityAt: minutesAgo(2) },
|
||||||
|
changedFiles: [
|
||||||
|
{ path: "frontend/src/renderer/lib/mock-data.ts", additions: 156, deletions: 22 },
|
||||||
|
{ path: "docs/readme.md", additions: 18, deletions: 4 },
|
||||||
|
],
|
||||||
|
commitMessage: "prepare readme screenshot data",
|
||||||
prs: [],
|
prs: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "profile-sheet",
|
id: "demo-needs-input",
|
||||||
workspaceId: "mobile-shell",
|
terminalHandleId: "demo-needs-input/terminal_0",
|
||||||
workspaceName: "mobile-shell",
|
workspaceId: "ao-demo",
|
||||||
title: "profile sheet accessibility pass",
|
workspaceName: "ao-demo",
|
||||||
|
title: "Resolve reviewer feedback on terminal polish",
|
||||||
provider: "claude-code",
|
provider: "claude-code",
|
||||||
branch: "fix/profile-sheet-a11y",
|
branch: "demo/terminal-polish",
|
||||||
|
status: "changes_requested",
|
||||||
|
displayStatus: "needs_you",
|
||||||
|
createdAt: hoursAgo(5),
|
||||||
|
updatedAt: minutesAgo(18),
|
||||||
|
activity: { state: "waiting_input", lastActivityAt: minutesAgo(18) },
|
||||||
|
changedFiles: [
|
||||||
|
{ path: "frontend/src/renderer/components/TerminalPane.tsx", additions: 41, deletions: 9 },
|
||||||
|
{ path: "frontend/src/renderer/styles.css", additions: 27, deletions: 3 },
|
||||||
|
],
|
||||||
|
commitMessage: "polish terminal screenshots",
|
||||||
|
prs: [demoPr(318, "open", "passing", "changes_requested")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "demo-review-stack",
|
||||||
|
terminalHandleId: "demo-review-stack/terminal_0",
|
||||||
|
workspaceId: "ao-demo",
|
||||||
|
workspaceName: "ao-demo",
|
||||||
|
title: "Review stacked browser preview flow",
|
||||||
|
provider: "codex",
|
||||||
|
branch: "demo/browser-preview-stack",
|
||||||
|
status: "review_pending",
|
||||||
|
displayStatus: "needs_you",
|
||||||
|
createdAt: hoursAgo(7),
|
||||||
|
updatedAt: minutesAgo(7),
|
||||||
|
activity: { state: "idle", lastActivityAt: minutesAgo(7) },
|
||||||
|
previewUrl: "http://localhost:5173",
|
||||||
|
previewRevision: 4,
|
||||||
|
changedFiles: [
|
||||||
|
{ path: "frontend/src/renderer/components/BrowserPanel.tsx", additions: 52, deletions: 11 },
|
||||||
|
{ path: "frontend/src/renderer/hooks/useBrowserView.ts", additions: 33, deletions: 6 },
|
||||||
|
{ path: "docs/assets/readme/browser-preview.png", additions: 1, deletions: 0 },
|
||||||
|
],
|
||||||
|
commitMessage: "wire readme browser preview",
|
||||||
|
prs: [
|
||||||
|
demoPr(319, "open", "passing", "none"),
|
||||||
|
demoPr(320, "open", "pending", "none", "unknown"),
|
||||||
|
demoPr(321, "draft", "pending", "none", "unknown"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "demo-in-review",
|
||||||
|
terminalHandleId: "demo-in-review/terminal_0",
|
||||||
|
workspaceId: "ao-demo",
|
||||||
|
workspaceName: "ao-demo",
|
||||||
|
title: "Wait for CI on project settings copy",
|
||||||
|
provider: "opencode",
|
||||||
|
branch: "demo/project-settings-copy",
|
||||||
|
status: "review_pending",
|
||||||
|
displayStatus: "unknown",
|
||||||
|
createdAt: hoursAgo(4),
|
||||||
|
updatedAt: minutesAgo(31),
|
||||||
|
activity: { state: "idle", lastActivityAt: minutesAgo(31) },
|
||||||
|
prs: [demoPr(322, "open", "pending", "none", "unknown")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "demo-ready",
|
||||||
|
terminalHandleId: "demo-ready/terminal_0",
|
||||||
|
workspaceId: "ao-demo",
|
||||||
|
workspaceName: "ao-demo",
|
||||||
|
title: "Merge README screenshot asset update",
|
||||||
|
provider: "codex",
|
||||||
|
branch: "demo/readme-assets",
|
||||||
status: "mergeable",
|
status: "mergeable",
|
||||||
updatedAt: hoursAgo(1.25),
|
displayStatus: "mergeable",
|
||||||
|
createdAt: hoursAgo(9),
|
||||||
|
updatedAt: minutesAgo(5),
|
||||||
|
activity: { state: "idle", lastActivityAt: minutesAgo(5) },
|
||||||
|
changedFiles: [
|
||||||
|
{ path: "docs/assets/readme/dashboard.png", additions: 1, deletions: 0 },
|
||||||
|
{ path: "docs/assets/readme/session-terminal.png", additions: 1, deletions: 0 },
|
||||||
|
],
|
||||||
|
prs: [demoPr(323, "open", "passing", "approved")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "demo-ci-failed",
|
||||||
|
terminalHandleId: "demo-ci-failed/terminal_0",
|
||||||
|
workspaceId: "ao-demo",
|
||||||
|
workspaceName: "ao-demo",
|
||||||
|
title: "Fix flaky NewTaskDialog smoke test",
|
||||||
|
provider: "codex",
|
||||||
|
branch: "demo/new-task-flake",
|
||||||
|
status: "ci_failed",
|
||||||
|
displayStatus: "needs_you",
|
||||||
createdAt: hoursAgo(8),
|
createdAt: hoursAgo(8),
|
||||||
prs: [pr(92, "open")],
|
updatedAt: minutesAgo(46),
|
||||||
|
activity: { state: "idle", lastActivityAt: minutesAgo(46) },
|
||||||
|
prs: [demoPr(324, "open", "failing", "none")],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "billing-portal",
|
id: "docs-site",
|
||||||
name: "billing-portal",
|
name: "docs-site",
|
||||||
path: "/Users/me/billing-portal",
|
path: "/demo/docs-site",
|
||||||
|
type: "main",
|
||||||
|
orchestratorAgent: "claude-code",
|
||||||
|
accentColor: "#93c5fd",
|
||||||
sessions: [
|
sessions: [
|
||||||
{
|
{
|
||||||
id: "invoice-export",
|
id: "docs-installation",
|
||||||
workspaceId: "billing-portal",
|
terminalHandleId: "docs-installation/terminal_0",
|
||||||
workspaceName: "billing-portal",
|
workspaceId: "docs-site",
|
||||||
title: "invoice CSV export",
|
workspaceName: "docs-site",
|
||||||
provider: "opencode",
|
title: "Tighten installation guide",
|
||||||
branch: "feat/invoice-export",
|
provider: "claude-code",
|
||||||
status: "review_pending",
|
branch: "demo/install-docs",
|
||||||
updatedAt: hoursAgo(2.25),
|
status: "working",
|
||||||
createdAt: hoursAgo(11),
|
createdAt: hoursAgo(2),
|
||||||
prs: [pr(117, "open")],
|
updatedAt: minutesAgo(13),
|
||||||
|
activity: { state: "active", lastActivityAt: minutesAgo(13) },
|
||||||
|
prs: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "tax-id-validation",
|
id: "docs-ready",
|
||||||
workspaceId: "billing-portal",
|
terminalHandleId: "docs-ready/terminal_0",
|
||||||
workspaceName: "billing-portal",
|
workspaceId: "docs-site",
|
||||||
title: "tax id validation errors",
|
workspaceName: "docs-site",
|
||||||
|
title: "Publish troubleshooting section",
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
branch: "fix/tax-id-validation",
|
branch: "demo/troubleshooting",
|
||||||
status: "needs_input",
|
status: "approved",
|
||||||
updatedAt: hoursAgo(1.75),
|
createdAt: hoursAgo(12),
|
||||||
createdAt: hoursAgo(5),
|
updatedAt: minutesAgo(22),
|
||||||
prs: [],
|
activity: { state: "idle", lastActivityAt: minutesAgo(22) },
|
||||||
|
prs: [demoPr(411, "open", "passing", "approved")],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,10 @@ export async function restartProjectOrchestrator({
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await refreshWorkspaceState(queryClient);
|
await refreshWorkspaceState(queryClient);
|
||||||
setOrchestratorReplacementError(projectId, error instanceof Error ? error.message : "Could not replace orchestrator");
|
setOrchestratorReplacementError(
|
||||||
|
projectId,
|
||||||
|
error instanceof Error ? error.message : "Could not replace orchestrator",
|
||||||
|
);
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
} finally {
|
} finally {
|
||||||
setProjectRestarting(projectId, false);
|
setProjectRestarting(projectId, false);
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,8 @@ describe("orchestratorHealth", () => {
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
state: "duplicates",
|
state: "duplicates",
|
||||||
message: "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
message:
|
||||||
|
"Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,8 @@ export function orchestratorHealth(workspace: WorkspaceSummary, restarting = fal
|
||||||
if (active.length > 1) {
|
if (active.length > 1) {
|
||||||
return {
|
return {
|
||||||
state: "duplicates",
|
state: "duplicates",
|
||||||
message: "Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
message:
|
||||||
|
"Multiple orchestrators are active. The newest one is used; stale ones will be cleaned up on daemon reconcile.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const orchestrator = newestActiveOrchestrator(workspace.sessions);
|
const orchestrator = newestActiveOrchestrator(workspace.sessions);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue