chore: format with prettier [skip ci]
This commit is contained in:
parent
2e5d66e398
commit
75fdf8941c
74
README.md
74
README.md
|
|
@ -49,17 +49,17 @@ An Agentic IDE that supervises parallel AI coding agents in isolated workspaces,
|
|||
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
| :--- | :--- |
|
||||
| **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 |
|
||||
| Feature | Description |
|
||||
| :----------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **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
|
||||
|
||||
|
|
@ -74,13 +74,14 @@ Works with 23+ CLI-based coding agents including Claude Code, OpenAI Codex, Curs
|
|||
### Prerequisites
|
||||
|
||||
| Requirement | Minimum | Recommended |
|
||||
|-------------|---------|-------------|
|
||||
| Go | 1.25+ | Latest |
|
||||
| Node.js | 20+ | Latest LTS |
|
||||
| Git | Any | Latest |
|
||||
| pnpm | Any | Latest |
|
||||
| ----------- | ------- | ----------- |
|
||||
| Go | 1.25+ | Latest |
|
||||
| Node.js | 20+ | Latest LTS |
|
||||
| Git | Any | Latest |
|
||||
| pnpm | Any | Latest |
|
||||
|
||||
**Optional:**
|
||||
|
||||
- `tmux` (Darwin/Linux) - For Unix runtime
|
||||
- `gh` (GitHub CLI) - For authenticated GitHub API calls
|
||||
|
||||
|
|
@ -88,11 +89,11 @@ Works with 23+ CLI-based coding agents including Claude Code, OpenAI Codex, Curs
|
|||
|
||||
Download the latest release for your platform:
|
||||
|
||||
| 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) |
|
||||
| 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) |
|
||||
|
||||
**Direct Download:** [Latest Release](https://github.com/AgentWrapper/agent-orchestrator/releases/latest)
|
||||
|
||||
|
|
@ -105,6 +106,7 @@ Agent Orchestrator is a long-running Go daemon built around **inbound/outbound p
|
|||
**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
|
||||
|
|
@ -117,12 +119,12 @@ For detailed architecture diagrams, data flows, and load-bearing rules, see [arc
|
|||
|
||||
## Documentation
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [Architecture](docs/architecture.md) | System architecture, data flows, and load-bearing rules |
|
||||
| [Backend Code Structure](docs/backend-code-structure.md) | Package-by-package ownership and dependency rules |
|
||||
| [AGENTS.md](AGENTS.md) | Contributor and worker-agent contract |
|
||||
| [Agent Adapter Contract](docs/agent/README.md) | Agent adapter interface and hook behavior |
|
||||
| Document | Description |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| [Architecture](docs/architecture.md) | System architecture, data flows, and load-bearing rules |
|
||||
| [Backend Code Structure](docs/backend-code-structure.md) | Package-by-package ownership and dependency rules |
|
||||
| [AGENTS.md](AGENTS.md) | Contributor and worker-agent contract |
|
||||
| [Agent Adapter Contract](docs/agent/README.md) | Agent adapter interface and hook behavior |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -147,15 +149,15 @@ npx @redwoodjs/agent-ci run --all
|
|||
|
||||
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 |
|
||||
| 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
|
||||
|
||||
|
|
|
|||
40
package.json
40
package.json
|
|
@ -1,22 +1,22 @@
|
|||
{
|
||||
"name": "agent-orchestrator",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"api": "run-s api:spec api:ts",
|
||||
"api:spec": "cd backend && go generate ./internal/httpd/apispec/...",
|
||||
"api:ts": "npx openapi-typescript@7.4.4 backend/internal/httpd/apispec/openapi.yaml -o packages/shared/src/api/schema.ts",
|
||||
"lint": "npm run lint:backend && npm run lint:frontend",
|
||||
"lint:backend": "cd backend && go test ./... && golangci-lint run",
|
||||
"lint:frontend": "cd packages/desktop && npm run typecheck",
|
||||
"build:shared": "cd packages/shared && npm run build",
|
||||
"build:desktop": "cd packages/desktop && npm run build",
|
||||
"build:mobile": "cd packages/mobile && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
"name": "agent-orchestrator",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"api": "run-s api:spec api:ts",
|
||||
"api:spec": "cd backend && go generate ./internal/httpd/apispec/...",
|
||||
"api:ts": "npx openapi-typescript@7.4.4 backend/internal/httpd/apispec/openapi.yaml -o packages/shared/src/api/schema.ts",
|
||||
"lint": "npm run lint:backend && npm run lint:frontend",
|
||||
"lint:backend": "cd backend && go test ./... && golangci-lint run",
|
||||
"lint:frontend": "cd packages/desktop && npm run typecheck",
|
||||
"build:shared": "cd packages/shared && npm run build",
|
||||
"build:desktop": "cd packages/desktop && npm run build",
|
||||
"build:mobile": "cd packages/mobile && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -8,188 +8,183 @@
|
|||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as ShellRouteImport } from './routes/_shell'
|
||||
import { Route as ShellIndexRouteImport } from './routes/_shell.index'
|
||||
import { Route as ShellPrsRouteImport } from './routes/_shell.prs'
|
||||
import { Route as ShellSessionsSessionIdRouteImport } from './routes/_shell.sessions.$sessionId'
|
||||
import { Route as ShellProjectsProjectIdRouteImport } from './routes/_shell.projects.$projectId'
|
||||
import { Route as ShellProjectsProjectIdSettingsRouteImport } from './routes/_shell.projects.$projectId_.settings'
|
||||
import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from './routes/_shell.projects.$projectId_.sessions.$sessionId'
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as ShellRouteImport } from "./routes/_shell";
|
||||
import { Route as ShellIndexRouteImport } from "./routes/_shell.index";
|
||||
import { Route as ShellPrsRouteImport } from "./routes/_shell.prs";
|
||||
import { Route as ShellSessionsSessionIdRouteImport } from "./routes/_shell.sessions.$sessionId";
|
||||
import { Route as ShellProjectsProjectIdRouteImport } from "./routes/_shell.projects.$projectId";
|
||||
import { Route as ShellProjectsProjectIdSettingsRouteImport } from "./routes/_shell.projects.$projectId_.settings";
|
||||
import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from "./routes/_shell.projects.$projectId_.sessions.$sessionId";
|
||||
|
||||
const ShellRoute = ShellRouteImport.update({
|
||||
id: '/_shell',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
id: "/_shell",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const ShellIndexRoute = ShellIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any)
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
const ShellPrsRoute = ShellPrsRouteImport.update({
|
||||
id: '/prs',
|
||||
path: '/prs',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any)
|
||||
id: "/prs",
|
||||
path: "/prs",
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
const ShellSessionsSessionIdRoute = ShellSessionsSessionIdRouteImport.update({
|
||||
id: '/sessions/$sessionId',
|
||||
path: '/sessions/$sessionId',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any)
|
||||
id: "/sessions/$sessionId",
|
||||
path: "/sessions/$sessionId",
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
const ShellProjectsProjectIdRoute = ShellProjectsProjectIdRouteImport.update({
|
||||
id: '/projects/$projectId',
|
||||
path: '/projects/$projectId',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any)
|
||||
const ShellProjectsProjectIdSettingsRoute =
|
||||
ShellProjectsProjectIdSettingsRouteImport.update({
|
||||
id: '/projects/$projectId_/settings',
|
||||
path: '/projects/$projectId/settings',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any)
|
||||
const ShellProjectsProjectIdSessionsSessionIdRoute =
|
||||
ShellProjectsProjectIdSessionsSessionIdRouteImport.update({
|
||||
id: '/projects/$projectId_/sessions/$sessionId',
|
||||
path: '/projects/$projectId/sessions/$sessionId',
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any)
|
||||
id: "/projects/$projectId",
|
||||
path: "/projects/$projectId",
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
const ShellProjectsProjectIdSettingsRoute = ShellProjectsProjectIdSettingsRouteImport.update({
|
||||
id: "/projects/$projectId_/settings",
|
||||
path: "/projects/$projectId/settings",
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
const ShellProjectsProjectIdSessionsSessionIdRoute = ShellProjectsProjectIdSessionsSessionIdRouteImport.update({
|
||||
id: "/projects/$projectId_/sessions/$sessionId",
|
||||
path: "/projects/$projectId/sessions/$sessionId",
|
||||
getParentRoute: () => ShellRoute,
|
||||
} as any);
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof ShellIndexRoute
|
||||
'/prs': typeof ShellPrsRoute
|
||||
'/projects/$projectId': typeof ShellProjectsProjectIdRoute
|
||||
'/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
|
||||
'/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute
|
||||
'/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
"/": typeof ShellIndexRoute;
|
||||
"/prs": typeof ShellPrsRoute;
|
||||
"/projects/$projectId": typeof ShellProjectsProjectIdRoute;
|
||||
"/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
|
||||
"/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
|
||||
"/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/prs': typeof ShellPrsRoute
|
||||
'/': typeof ShellIndexRoute
|
||||
'/projects/$projectId': typeof ShellProjectsProjectIdRoute
|
||||
'/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
|
||||
'/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute
|
||||
'/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
"/prs": typeof ShellPrsRoute;
|
||||
"/": typeof ShellIndexRoute;
|
||||
"/projects/$projectId": typeof ShellProjectsProjectIdRoute;
|
||||
"/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
|
||||
"/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute;
|
||||
"/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_shell': typeof ShellRouteWithChildren
|
||||
'/_shell/prs': typeof ShellPrsRoute
|
||||
'/_shell/': typeof ShellIndexRoute
|
||||
'/_shell/projects/$projectId': typeof ShellProjectsProjectIdRoute
|
||||
'/_shell/sessions/$sessionId': typeof ShellSessionsSessionIdRoute
|
||||
'/_shell/projects/$projectId_/settings': typeof ShellProjectsProjectIdSettingsRoute
|
||||
'/_shell/projects/$projectId_/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
__root__: typeof rootRouteImport;
|
||||
"/_shell": typeof ShellRouteWithChildren;
|
||||
"/_shell/prs": typeof ShellPrsRoute;
|
||||
"/_shell/": typeof ShellIndexRoute;
|
||||
"/_shell/projects/$projectId": typeof ShellProjectsProjectIdRoute;
|
||||
"/_shell/sessions/$sessionId": typeof ShellSessionsSessionIdRoute;
|
||||
"/_shell/projects/$projectId_/settings": typeof ShellProjectsProjectIdSettingsRoute;
|
||||
"/_shell/projects/$projectId_/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/prs'
|
||||
| '/projects/$projectId'
|
||||
| '/sessions/$sessionId'
|
||||
| '/projects/$projectId/settings'
|
||||
| '/projects/$projectId/sessions/$sessionId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/prs'
|
||||
| '/'
|
||||
| '/projects/$projectId'
|
||||
| '/sessions/$sessionId'
|
||||
| '/projects/$projectId/settings'
|
||||
| '/projects/$projectId/sessions/$sessionId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/_shell'
|
||||
| '/_shell/prs'
|
||||
| '/_shell/'
|
||||
| '/_shell/projects/$projectId'
|
||||
| '/_shell/sessions/$sessionId'
|
||||
| '/_shell/projects/$projectId_/settings'
|
||||
| '/_shell/projects/$projectId_/sessions/$sessionId'
|
||||
fileRoutesById: FileRoutesById
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths:
|
||||
| "/"
|
||||
| "/prs"
|
||||
| "/projects/$projectId"
|
||||
| "/sessions/$sessionId"
|
||||
| "/projects/$projectId/settings"
|
||||
| "/projects/$projectId/sessions/$sessionId";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to:
|
||||
| "/prs"
|
||||
| "/"
|
||||
| "/projects/$projectId"
|
||||
| "/sessions/$sessionId"
|
||||
| "/projects/$projectId/settings"
|
||||
| "/projects/$projectId/sessions/$sessionId";
|
||||
id:
|
||||
| "__root__"
|
||||
| "/_shell"
|
||||
| "/_shell/prs"
|
||||
| "/_shell/"
|
||||
| "/_shell/projects/$projectId"
|
||||
| "/_shell/sessions/$sessionId"
|
||||
| "/_shell/projects/$projectId_/settings"
|
||||
| "/_shell/projects/$projectId_/sessions/$sessionId";
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
ShellRoute: typeof ShellRouteWithChildren
|
||||
ShellRoute: typeof ShellRouteWithChildren;
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/_shell': {
|
||||
id: '/_shell'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof ShellRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_shell/': {
|
||||
id: '/_shell/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof ShellIndexRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/prs': {
|
||||
id: '/_shell/prs'
|
||||
path: '/prs'
|
||||
fullPath: '/prs'
|
||||
preLoaderRoute: typeof ShellPrsRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/sessions/$sessionId': {
|
||||
id: '/_shell/sessions/$sessionId'
|
||||
path: '/sessions/$sessionId'
|
||||
fullPath: '/sessions/$sessionId'
|
||||
preLoaderRoute: typeof ShellSessionsSessionIdRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/projects/$projectId': {
|
||||
id: '/_shell/projects/$projectId'
|
||||
path: '/projects/$projectId'
|
||||
fullPath: '/projects/$projectId'
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/projects/$projectId_/settings': {
|
||||
id: '/_shell/projects/$projectId_/settings'
|
||||
path: '/projects/$projectId/settings'
|
||||
fullPath: '/projects/$projectId/settings'
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
'/_shell/projects/$projectId_/sessions/$sessionId': {
|
||||
id: '/_shell/projects/$projectId_/sessions/$sessionId'
|
||||
path: '/projects/$projectId/sessions/$sessionId'
|
||||
fullPath: '/projects/$projectId/sessions/$sessionId'
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport
|
||||
parentRoute: typeof ShellRoute
|
||||
}
|
||||
}
|
||||
declare module "@tanstack/react-router" {
|
||||
interface FileRoutesByPath {
|
||||
"/_shell": {
|
||||
id: "/_shell";
|
||||
path: "";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof ShellRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/_shell/": {
|
||||
id: "/_shell/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof ShellIndexRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/prs": {
|
||||
id: "/_shell/prs";
|
||||
path: "/prs";
|
||||
fullPath: "/prs";
|
||||
preLoaderRoute: typeof ShellPrsRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/sessions/$sessionId": {
|
||||
id: "/_shell/sessions/$sessionId";
|
||||
path: "/sessions/$sessionId";
|
||||
fullPath: "/sessions/$sessionId";
|
||||
preLoaderRoute: typeof ShellSessionsSessionIdRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/projects/$projectId": {
|
||||
id: "/_shell/projects/$projectId";
|
||||
path: "/projects/$projectId";
|
||||
fullPath: "/projects/$projectId";
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/projects/$projectId_/settings": {
|
||||
id: "/_shell/projects/$projectId_/settings";
|
||||
path: "/projects/$projectId/settings";
|
||||
fullPath: "/projects/$projectId/settings";
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
"/_shell/projects/$projectId_/sessions/$sessionId": {
|
||||
id: "/_shell/projects/$projectId_/sessions/$sessionId";
|
||||
path: "/projects/$projectId/sessions/$sessionId";
|
||||
fullPath: "/projects/$projectId/sessions/$sessionId";
|
||||
preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport;
|
||||
parentRoute: typeof ShellRoute;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ShellRouteChildren {
|
||||
ShellPrsRoute: typeof ShellPrsRoute
|
||||
ShellIndexRoute: typeof ShellIndexRoute
|
||||
ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute
|
||||
ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute
|
||||
ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute
|
||||
ShellPrsRoute: typeof ShellPrsRoute;
|
||||
ShellIndexRoute: typeof ShellIndexRoute;
|
||||
ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute;
|
||||
ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute;
|
||||
ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute;
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute;
|
||||
}
|
||||
|
||||
const ShellRouteChildren: ShellRouteChildren = {
|
||||
ShellPrsRoute: ShellPrsRoute,
|
||||
ShellIndexRoute: ShellIndexRoute,
|
||||
ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute,
|
||||
ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute,
|
||||
ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute,
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute:
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute,
|
||||
}
|
||||
ShellPrsRoute: ShellPrsRoute,
|
||||
ShellIndexRoute: ShellIndexRoute,
|
||||
ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute,
|
||||
ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute,
|
||||
ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute,
|
||||
ShellProjectsProjectIdSessionsSessionIdRoute: ShellProjectsProjectIdSessionsSessionIdRoute,
|
||||
};
|
||||
|
||||
const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren)
|
||||
const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren);
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
ShellRoute: ShellRouteWithChildren,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
ShellRoute: ShellRouteWithChildren,
|
||||
};
|
||||
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
preset: 'ts-jest/presets/default',
|
||||
testEnvironment: 'jsdom',
|
||||
testMatch: ['**/src/**/*.test.ts', '**/src/**/*.test.tsx'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
'^react$': '<rootDir>/node_modules/react',
|
||||
'^react-dom$': '<rootDir>/node_modules/react-dom',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: {
|
||||
types: ['jest'],
|
||||
jsx: 'react',
|
||||
module: 'ESNext',
|
||||
moduleResolution: 'bundler',
|
||||
allowSyntheticDefaultImports: true,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(react|react-dom|@tanstack/react-query|@testing-library)/)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
|
||||
moduleDirectories: ['node_modules', '<rootDir>/node_modules'],
|
||||
};
|
||||
preset: "ts-jest/presets/default",
|
||||
testEnvironment: "jsdom",
|
||||
testMatch: ["**/src/**/*.test.ts", "**/src/**/*.test.tsx"],
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
"^react$": "<rootDir>/node_modules/react",
|
||||
"^react-dom$": "<rootDir>/node_modules/react-dom",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: {
|
||||
types: ["jest"],
|
||||
jsx: "react",
|
||||
module: "ESNext",
|
||||
moduleResolution: "bundler",
|
||||
allowSyntheticDefaultImports: true,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ["node_modules/(?!(react|react-dom|@tanstack/react-query|@testing-library)/)"],
|
||||
setupFilesAfterEnv: ["<rootDir>/jest.setup.cjs"],
|
||||
moduleDirectories: ["node_modules", "<rootDir>/node_modules"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
module.exports = require('@testing-library/jest-dom');
|
||||
module.exports = require("@testing-library/jest-dom");
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
{
|
||||
"name": "@agent-orchestrator/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"eventsource": "^2.0.0",
|
||||
"zustand": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^14.3.1",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^30.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
"name": "@agent-orchestrator/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"eventsource": "^2.0.0",
|
||||
"zustand": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^14.3.1",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^30.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +1,97 @@
|
|||
// packages/shared/src/api/client.ts
|
||||
import { getPlatformBridge } from '../lib/bridge';
|
||||
import { getPlatformBridge } from "../lib/bridge";
|
||||
|
||||
export interface RequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
body?: any;
|
||||
headers?: Record<string, string>;
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||
body?: any;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public statusText: string,
|
||||
public body?: any
|
||||
) {
|
||||
super(`API Error ${status}: ${statusText}`);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
constructor(
|
||||
public status: number,
|
||||
public statusText: string,
|
||||
public body?: any,
|
||||
) {
|
||||
super(`API Error ${status}: ${statusText}`);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private authToken?: string;
|
||||
private authToken?: string;
|
||||
|
||||
constructor(authToken?: string) {
|
||||
this.authToken = authToken;
|
||||
}
|
||||
constructor(authToken?: string) {
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
private getBaseUrl(): string {
|
||||
return getPlatformBridge().getApiBaseUrl();
|
||||
}
|
||||
private getBaseUrl(): string {
|
||||
return getPlatformBridge().getApiBaseUrl();
|
||||
}
|
||||
|
||||
private getDefaultHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
private getDefaultHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (this.authToken) {
|
||||
headers['Authorization'] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
if (this.authToken) {
|
||||
headers["Authorization"] = `Bearer ${this.authToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const baseUrl = this.getBaseUrl();
|
||||
const url = `${baseUrl}${path}`;
|
||||
async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const baseUrl = this.getBaseUrl();
|
||||
const url = `${baseUrl}${path}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
...this.getDefaultHeaders(),
|
||||
...options.headers,
|
||||
},
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
const response = await fetch(url, {
|
||||
method: options.method || "GET",
|
||||
headers: {
|
||||
...this.getDefaultHeaders(),
|
||||
...options.headers,
|
||||
},
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => undefined);
|
||||
throw new ApiError(response.status, response.statusText, body);
|
||||
}
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => undefined);
|
||||
throw new ApiError(response.status, response.statusText, body);
|
||||
}
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async get<T>(path: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: 'GET', headers });
|
||||
}
|
||||
async get<T>(path: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: "GET", headers });
|
||||
}
|
||||
|
||||
async post<T>(path: string, body: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: 'POST', body, headers });
|
||||
}
|
||||
async post<T>(path: string, body: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: "POST", body, headers });
|
||||
}
|
||||
|
||||
async put<T>(path: string, body: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: 'PUT', body, headers });
|
||||
}
|
||||
async put<T>(path: string, body: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: "PUT", body, headers });
|
||||
}
|
||||
|
||||
async delete<T>(path: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: 'DELETE', headers });
|
||||
}
|
||||
async delete<T>(path: string, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: "DELETE", headers });
|
||||
}
|
||||
|
||||
async patch<T>(path: string, body: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: 'PATCH', body, headers });
|
||||
}
|
||||
async patch<T>(path: string, body: any, headers?: Record<string, string>): Promise<T> {
|
||||
return this.request<T>(path, { method: "PATCH", body, headers });
|
||||
}
|
||||
|
||||
setAuthToken(token: string): void {
|
||||
this.authToken = token;
|
||||
}
|
||||
setAuthToken(token: string): void {
|
||||
this.authToken = token;
|
||||
}
|
||||
|
||||
clearAuthToken(): void {
|
||||
this.authToken = undefined;
|
||||
}
|
||||
clearAuthToken(): void {
|
||||
this.authToken = undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// packages/shared/src/api/paths.ts
|
||||
// API path constants
|
||||
export const API_PATHS = {
|
||||
workspaces: '/api/v1/workspaces',
|
||||
sessions: '/api/v1/sessions',
|
||||
workspaces: "/api/v1/workspaces",
|
||||
sessions: "/api/v1/sessions",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
// This file is a placeholder for generated types
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
error: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
error: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
// packages/shared/src/hooks/useDaemonStatus.ts
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getPlatformBridge, DaemonStatus } from '../lib/bridge';
|
||||
import { useEffect, useState } from "react";
|
||||
import { getPlatformBridge, DaemonStatus } from "../lib/bridge";
|
||||
|
||||
export function useDaemonStatus() {
|
||||
const [status, setStatus] = useState<DaemonStatus>({ running: false, port: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [status, setStatus] = useState<DaemonStatus>({ running: false, port: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const bridge = getPlatformBridge();
|
||||
const currentStatus = await bridge.getDaemonStatus();
|
||||
if (mounted) {
|
||||
setStatus(currentStatus);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
setStatus({ running: false, port: 0 });
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const bridge = getPlatformBridge();
|
||||
const currentStatus = await bridge.getDaemonStatus();
|
||||
if (mounted) {
|
||||
setStatus(currentStatus);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
setStatus({ running: false, port: 0 });
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
checkStatus();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { status, loading };
|
||||
return { status, loading };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,52 @@
|
|||
// packages/shared/src/hooks/useWorkspaceQuery.test.tsx
|
||||
import React from 'react';
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useWorkspaceQuery } from './useWorkspaceQuery';
|
||||
import { ApiClient } from '../api/client';
|
||||
import { setPlatformBridge } from '../lib/bridge';
|
||||
import React from "react";
|
||||
import { describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useWorkspaceQuery } from "./useWorkspaceQuery";
|
||||
import { ApiClient } from "../api/client";
|
||||
import { setPlatformBridge } from "../lib/bridge";
|
||||
|
||||
describe('useWorkspaceQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
describe("useWorkspaceQuery", () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
setPlatformBridge({
|
||||
getDaemonStatus: async () => ({ running: true, port: 3001 }),
|
||||
startDaemon: async () => {},
|
||||
stopDaemon: async () => {},
|
||||
getApiBaseUrl: () => 'http://test:3001',
|
||||
subscribeApiBaseUrl: () => () => {},
|
||||
});
|
||||
});
|
||||
setPlatformBridge({
|
||||
getDaemonStatus: async () => ({ running: true, port: 3001 }),
|
||||
startDaemon: async () => {},
|
||||
stopDaemon: async () => {},
|
||||
getApiBaseUrl: () => "http://test:3001",
|
||||
subscribeApiBaseUrl: () => () => {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch workspace data', async () => {
|
||||
const mockWorkspaces = [
|
||||
{ id: '1', name: 'Project A', sessions: [] },
|
||||
{ id: '2', name: 'Project B', sessions: [] },
|
||||
];
|
||||
it("should fetch workspace data", async () => {
|
||||
const mockWorkspaces = [
|
||||
{ id: "1", name: "Project A", sessions: [] },
|
||||
{ id: "2", name: "Project B", sessions: [] },
|
||||
];
|
||||
|
||||
globalThis.fetch = globalThis.fetch || jest.fn();
|
||||
(globalThis.fetch as jest.Mock).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ workspaces: mockWorkspaces }),
|
||||
});
|
||||
globalThis.fetch = globalThis.fetch || jest.fn();
|
||||
(globalThis.fetch as jest.Mock).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ workspaces: mockWorkspaces }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useWorkspaceQuery(), {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
const { result } = renderHook(() => useWorkspaceQuery(), {
|
||||
wrapper: ({ children }) => <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>,
|
||||
});
|
||||
|
||||
// Trigger the query and wait for it to complete
|
||||
const refetchPromise = result.current.refetch();
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
await refetchPromise;
|
||||
// Trigger the query and wait for it to complete
|
||||
const refetchPromise = result.current.refetch();
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
await refetchPromise;
|
||||
|
||||
expect(result.current.data).toEqual({ workspaces: mockWorkspaces });
|
||||
});
|
||||
expect(result.current.data).toEqual({ workspaces: mockWorkspaces });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
// packages/shared/src/hooks/useWorkspaceQuery.ts
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ApiClient } from '../api/client';
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ApiClient } from "../api/client";
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
projectId: string;
|
||||
agent: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
projectId: string;
|
||||
agent: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
sessions: Session[];
|
||||
id: string;
|
||||
name: string;
|
||||
sessions: Session[];
|
||||
}
|
||||
|
||||
export interface WorkspaceResponse {
|
||||
workspaces: Project[];
|
||||
workspaces: Project[];
|
||||
}
|
||||
|
||||
const WORKSPACE_QUERY_KEY = ['workspaces'] as const;
|
||||
const WORKSPACE_QUERY_KEY = ["workspaces"] as const;
|
||||
|
||||
export function useWorkspaceQuery(client?: ApiClient) {
|
||||
const apiClient = client || new ApiClient();
|
||||
const apiClient = client || new ApiClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: WORKSPACE_QUERY_KEY,
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get<WorkspaceResponse>('/api/v1/workspaces');
|
||||
return response;
|
||||
},
|
||||
enabled: false, // Manually refetch on SSE events
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const query = useQuery({
|
||||
queryKey: WORKSPACE_QUERY_KEY,
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get<WorkspaceResponse>("/api/v1/workspaces");
|
||||
return response;
|
||||
},
|
||||
enabled: false, // Manually refetch on SSE events
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return {
|
||||
...query,
|
||||
refetch: () => query.refetch(),
|
||||
};
|
||||
return {
|
||||
...query,
|
||||
refetch: () => query.refetch(),
|
||||
};
|
||||
}
|
||||
|
||||
export { WORKSPACE_QUERY_KEY };
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// packages/shared/src/index.ts
|
||||
export * from './api/client';
|
||||
export * from './api/paths';
|
||||
export * from './api/types';
|
||||
export * from './lib/bridge';
|
||||
export * from './lib/event-transport';
|
||||
export * from './lib/events-connection';
|
||||
export * from './hooks/useWorkspaceQuery';
|
||||
export * from './hooks/useDaemonStatus';
|
||||
export * from "./api/client";
|
||||
export * from "./api/paths";
|
||||
export * from "./api/types";
|
||||
export * from "./lib/bridge";
|
||||
export * from "./lib/event-transport";
|
||||
export * from "./lib/events-connection";
|
||||
export * from "./hooks/useWorkspaceQuery";
|
||||
export * from "./hooks/useDaemonStatus";
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
export interface DaemonStatus {
|
||||
running: boolean;
|
||||
port: number;
|
||||
running: boolean;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface PlatformBridge {
|
||||
// Daemon lifecycle
|
||||
getDaemonStatus: () => Promise<DaemonStatus>;
|
||||
startDaemon: () => Promise<void>;
|
||||
stopDaemon: () => Promise<void>;
|
||||
// Daemon lifecycle
|
||||
getDaemonStatus: () => Promise<DaemonStatus>;
|
||||
startDaemon: () => Promise<void>;
|
||||
stopDaemon: () => Promise<void>;
|
||||
|
||||
// API base URL resolution
|
||||
getApiBaseUrl: () => string;
|
||||
subscribeApiBaseUrl: (callback: (url: string) => void) => () => void;
|
||||
// API base URL resolution
|
||||
getApiBaseUrl: () => string;
|
||||
subscribeApiBaseUrl: (callback: (url: string) => void) => () => void;
|
||||
}
|
||||
|
||||
let platformBridgeInstance: PlatformBridge | null = null;
|
||||
|
||||
export function setPlatformBridge(bridge: PlatformBridge): void {
|
||||
platformBridgeInstance = bridge;
|
||||
platformBridgeInstance = bridge;
|
||||
}
|
||||
|
||||
export function getPlatformBridge(): PlatformBridge {
|
||||
if (!platformBridgeInstance) {
|
||||
throw new Error('PlatformBridge not initialized. Call setPlatformBridge() first.');
|
||||
}
|
||||
return platformBridgeInstance;
|
||||
if (!platformBridgeInstance) {
|
||||
throw new Error("PlatformBridge not initialized. Call setPlatformBridge() first.");
|
||||
}
|
||||
return platformBridgeInstance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
// packages/shared/src/lib/event-transport.test.ts
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { createEventTransport } from './event-transport';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { setPlatformBridge } from './bridge';
|
||||
import { describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import { createEventTransport } from "./event-transport";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { setPlatformBridge } from "./bridge";
|
||||
|
||||
describe('EventTransport', () => {
|
||||
let queryClient: QueryClient;
|
||||
describe("EventTransport", () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient();
|
||||
setPlatformBridge({
|
||||
getDaemonStatus: async () => ({ running: true, port: 3001 }),
|
||||
startDaemon: async () => {},
|
||||
stopDaemon: async () => {},
|
||||
getApiBaseUrl: () => 'http://test:3001',
|
||||
subscribeApiBaseUrl: () => () => {},
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient();
|
||||
setPlatformBridge({
|
||||
getDaemonStatus: async () => ({ running: true, port: 3001 }),
|
||||
startDaemon: async () => {},
|
||||
stopDaemon: async () => {},
|
||||
getApiBaseUrl: () => "http://test:3001",
|
||||
subscribeApiBaseUrl: () => () => {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create transport with connect method', () => {
|
||||
const transport = createEventTransport(queryClient);
|
||||
expect(typeof transport.connect).toBe('function');
|
||||
});
|
||||
it("should create transport with connect method", () => {
|
||||
const transport = createEventTransport(queryClient);
|
||||
expect(typeof transport.connect).toBe("function");
|
||||
});
|
||||
|
||||
it('should return cleanup function from connect', () => {
|
||||
const transport = createEventTransport(queryClient);
|
||||
const cleanup = transport.connect();
|
||||
expect(typeof cleanup).toBe('function');
|
||||
});
|
||||
it("should return cleanup function from connect", () => {
|
||||
const transport = createEventTransport(queryClient);
|
||||
const cleanup = transport.connect();
|
||||
expect(typeof cleanup).toBe("function");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// packages/shared/src/lib/event-transport.ts
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import { getPlatformBridge } from './bridge';
|
||||
import { setEventsConnectionState } from './events-connection';
|
||||
import { WORKSPACE_QUERY_KEY } from '../hooks/useWorkspaceQuery';
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { getPlatformBridge } from "./bridge";
|
||||
import { setEventsConnectionState } from "./events-connection";
|
||||
import { WORKSPACE_QUERY_KEY } from "../hooks/useWorkspaceQuery";
|
||||
|
||||
export type EventTransport = {
|
||||
connect: () => () => void;
|
||||
connect: () => () => void;
|
||||
};
|
||||
|
||||
const INVALIDATE_DEBOUNCE_MS = 150;
|
||||
|
|
@ -13,77 +13,77 @@ const SSE_RETRY_MS = 5_000;
|
|||
const EVENTSOURCE_CLOSED = 2;
|
||||
|
||||
const CDC_EVENT_TYPES = [
|
||||
'session_created',
|
||||
'session_updated',
|
||||
'pr_created',
|
||||
'pr_updated',
|
||||
'pr_check_recorded',
|
||||
'pr_session_changed',
|
||||
'pr_review_thread_added',
|
||||
'pr_review_thread_resolved',
|
||||
"session_created",
|
||||
"session_updated",
|
||||
"pr_created",
|
||||
"pr_updated",
|
||||
"pr_check_recorded",
|
||||
"pr_session_changed",
|
||||
"pr_review_thread_added",
|
||||
"pr_review_thread_resolved",
|
||||
] as const;
|
||||
|
||||
export function createEventTransport(queryClient: QueryClient): EventTransport {
|
||||
return {
|
||||
connect() {
|
||||
let debounce: ReturnType<typeof setTimeout> | undefined;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let source: EventSource | undefined;
|
||||
let sourceBaseUrl: string | undefined;
|
||||
return {
|
||||
connect() {
|
||||
let debounce: ReturnType<typeof setTimeout> | undefined;
|
||||
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let source: EventSource | undefined;
|
||||
let sourceBaseUrl: string | undefined;
|
||||
|
||||
const refreshWorkspaces = () => {
|
||||
if (debounce) clearTimeout(debounce);
|
||||
debounce = setTimeout(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: WORKSPACE_QUERY_KEY });
|
||||
}, INVALIDATE_DEBOUNCE_MS);
|
||||
};
|
||||
const refreshWorkspaces = () => {
|
||||
if (debounce) clearTimeout(debounce);
|
||||
debounce = setTimeout(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: WORKSPACE_QUERY_KEY });
|
||||
}, INVALIDATE_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
const scheduleRetry = () => {
|
||||
if (retryTimer) return;
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = undefined;
|
||||
connectSource();
|
||||
}, SSE_RETRY_MS);
|
||||
};
|
||||
const scheduleRetry = () => {
|
||||
if (retryTimer) return;
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = undefined;
|
||||
connectSource();
|
||||
}, SSE_RETRY_MS);
|
||||
};
|
||||
|
||||
const bridge = getPlatformBridge();
|
||||
const bridge = getPlatformBridge();
|
||||
|
||||
const connectSource = () => {
|
||||
if (typeof EventSource === 'undefined') return;
|
||||
const baseUrl = bridge.getApiBaseUrl();
|
||||
if (source && sourceBaseUrl === baseUrl && source.readyState !== EVENTSOURCE_CLOSED) return;
|
||||
source?.close();
|
||||
source = undefined;
|
||||
sourceBaseUrl = baseUrl;
|
||||
try {
|
||||
source = new EventSource(`${baseUrl.replace(/\/+$/, '')}/api/v1/events`);
|
||||
source.onopen = () => {
|
||||
setEventsConnectionState('connected');
|
||||
refreshWorkspaces();
|
||||
};
|
||||
source.onerror = () => {
|
||||
setEventsConnectionState('disconnected');
|
||||
if (source?.readyState === EVENTSOURCE_CLOSED) scheduleRetry();
|
||||
};
|
||||
source.onmessage = refreshWorkspaces;
|
||||
for (const type of CDC_EVENT_TYPES) {
|
||||
source.addEventListener(type, refreshWorkspaces);
|
||||
}
|
||||
} catch {
|
||||
source = undefined;
|
||||
}
|
||||
};
|
||||
const connectSource = () => {
|
||||
if (typeof EventSource === "undefined") return;
|
||||
const baseUrl = bridge.getApiBaseUrl();
|
||||
if (source && sourceBaseUrl === baseUrl && source.readyState !== EVENTSOURCE_CLOSED) return;
|
||||
source?.close();
|
||||
source = undefined;
|
||||
sourceBaseUrl = baseUrl;
|
||||
try {
|
||||
source = new EventSource(`${baseUrl.replace(/\/+$/, "")}/api/v1/events`);
|
||||
source.onopen = () => {
|
||||
setEventsConnectionState("connected");
|
||||
refreshWorkspaces();
|
||||
};
|
||||
source.onerror = () => {
|
||||
setEventsConnectionState("disconnected");
|
||||
if (source?.readyState === EVENTSOURCE_CLOSED) scheduleRetry();
|
||||
};
|
||||
source.onmessage = refreshWorkspaces;
|
||||
for (const type of CDC_EVENT_TYPES) {
|
||||
source.addEventListener(type, refreshWorkspaces);
|
||||
}
|
||||
} catch {
|
||||
source = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const removeBaseUrlListener = bridge.subscribeApiBaseUrl(connectSource);
|
||||
connectSource();
|
||||
const removeBaseUrlListener = bridge.subscribeApiBaseUrl(connectSource);
|
||||
connectSource();
|
||||
|
||||
return () => {
|
||||
if (debounce) clearTimeout(debounce);
|
||||
if (retryTimer) clearTimeout(retryTimer);
|
||||
removeBaseUrlListener();
|
||||
source?.close();
|
||||
setEventsConnectionState('idle');
|
||||
};
|
||||
},
|
||||
};
|
||||
return () => {
|
||||
if (debounce) clearTimeout(debounce);
|
||||
if (retryTimer) clearTimeout(retryTimer);
|
||||
removeBaseUrlListener();
|
||||
source?.close();
|
||||
setEventsConnectionState("idle");
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
// packages/shared/src/lib/events-connection.ts
|
||||
type ConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected';
|
||||
type ConnectionState = "idle" | "connecting" | "connected" | "disconnected";
|
||||
|
||||
let connectionState: ConnectionState = 'idle';
|
||||
let connectionState: ConnectionState = "idle";
|
||||
const listeners: Set<(state: ConnectionState) => void> = new Set();
|
||||
|
||||
export function setEventsConnectionState(state: ConnectionState): void {
|
||||
connectionState = state;
|
||||
listeners.forEach((listener) => listener(state));
|
||||
connectionState = state;
|
||||
listeners.forEach((listener) => listener(state));
|
||||
}
|
||||
|
||||
export function getEventsConnectionState(): ConnectionState {
|
||||
return connectionState;
|
||||
return connectionState;
|
||||
}
|
||||
|
||||
export function subscribeEventsConnectionState(
|
||||
callback: (state: ConnectionState) => void
|
||||
): () => void {
|
||||
listeners.add(callback);
|
||||
callback(connectionState);
|
||||
return () => listeners.delete(callback);
|
||||
export function subscribeEventsConnectionState(callback: (state: ConnectionState) => void): () => void {
|
||||
listeners.add(callback);
|
||||
callback(connectionState);
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue