mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-24 05:28:46 +02:00
docs: add architecture guide
Create ARCHITECTURE.md with a concrete overview of the Electron app, its renderer/main/preload boundaries, shared domain model, .NET sidecar, persistence model, workflow execution path, tooling model, and build/release pipeline. Also update AGENTS.md so future agents keep ARCHITECTURE.md in sync when a change materially affects the system design. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -41,6 +41,7 @@ These instructions apply to any automated or semi-automated agent working in thi
|
||||
- Keep commits atomic. Each commit should represent one logical change and use a Conventional Commit message such as `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, or `chore:`.
|
||||
- Treat tests as part of the implementation, not as follow-up work. Every fix, behavior change, and new feature must be covered by tests, and the relevant test suite must pass before the work is considered complete.
|
||||
- Always check whether `README.md` needs an update before handing work off. If the change affects user-facing behavior, workflows, prerequisites, installation, packaging, or product positioning, update the README in the same change.
|
||||
- Always check whether `ARCHITECTURE.md` needs an update before handing work off. If the change affects runtime boundaries, data flow, persistence, IPC, orchestration, tooling integration, packaging, or other material technical design, update `ARCHITECTURE.md` in the same change.
|
||||
- Do not ship quick fixes, hacks, or "temporary" patches as final solutions. Take the time to understand the problem, plan the change, and implement a maintainable solution that fits the codebase cleanly.
|
||||
- Remove code that is no longer necessary before handing work off. If an experiment, workaround, hotfix, helper, or test path does not end up being part of the final correct solution, delete it rather than leaving dead or misleading code behind.
|
||||
- Apply the same quality bar to feature work. Think through scope, edge cases, integration points, and long-term maintainability before implementing.
|
||||
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
# Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Eryx is a desktop Electron application for Copilot-powered work across a scratchpad and project-backed sessions. The codebase is intentionally split into five major layers:
|
||||
|
||||
- an Electron **main process** that owns native integration, persistence, git inspection, and sidecar lifecycle
|
||||
- a React **renderer** that owns the user interface
|
||||
- a **preload bridge** that exposes a typed IPC API to the renderer under context isolation
|
||||
- a shared TypeScript **domain/contracts** layer used on both sides of the Electron boundary
|
||||
- a .NET **sidecar** process that talks to GitHub Copilot and Microsoft Agent Framework over stdio
|
||||
|
||||
This split keeps the UI process unprivileged while concentrating filesystem, process, and workflow orchestration in the main process and sidecar.
|
||||
|
||||
Key entry points:
|
||||
|
||||
- `src/main/index.ts:11-42`
|
||||
- `src/main/windows/createMainWindow.ts:6-46`
|
||||
- `src/preload/index.ts:6-50`
|
||||
- `src/renderer/App.tsx:83-153`
|
||||
- `sidecar/src/Eryx.AgentHost/Program.cs:1-10`
|
||||
|
||||
## High-level runtime topology
|
||||
|
||||
```text
|
||||
Renderer (React + Tailwind)
|
||||
|
|
||||
| window.eryxApi
|
||||
v
|
||||
Preload bridge
|
||||
|
|
||||
| ipcRenderer.invoke / ipcMain.handle
|
||||
v
|
||||
Electron main process
|
||||
- EryxAppService
|
||||
- WorkspaceRepository
|
||||
- GitService
|
||||
- SidecarClient
|
||||
|
|
||||
| JSON lines over stdio
|
||||
v
|
||||
.NET sidecar (Eryx.AgentHost)
|
||||
- SidecarProtocolHost
|
||||
- CopilotWorkflowRunner
|
||||
|
|
||||
v
|
||||
GitHub Copilot CLI + Microsoft Agent Framework
|
||||
```
|
||||
|
||||
## Source layout
|
||||
|
||||
### Main process
|
||||
|
||||
- `src/main/index.ts` boots the app, creates the main window, registers IPC handlers, and applies the persisted title-bar theme (`src/main/index.ts:11-24`).
|
||||
- `src/main/windows/createMainWindow.ts` creates the `BrowserWindow` with `contextIsolation: true`, `nodeIntegration: false`, a hidden native title bar, and an Electron title bar overlay (`src/main/windows/createMainWindow.ts:9-30`).
|
||||
- `src/main/ipc/registerIpcHandlers.ts` maps renderer IPC calls onto `EryxAppService` methods and forwards service events back to the renderer (`src/main/ipc/registerIpcHandlers.ts:24-97`).
|
||||
- `src/main/EryxAppService.ts` is the main application service. It owns workspace mutation, session lifecycle, tool selection, sidecar orchestration, and event emission (`src/main/EryxAppService.ts:82-117`, `src/main/EryxAppService.ts:317-474`, `src/main/EryxAppService.ts:837-943`).
|
||||
- `src/main/persistence/` contains JSON-backed workspace persistence and app-specific paths (`src/main/persistence/appPaths.ts:4-10`, `src/main/persistence/jsonStore.ts:4-20`, `src/main/persistence/workspaceRepository.ts:38-86`).
|
||||
- `src/main/git/gitService.ts` derives repository status, branch, upstream, ahead/behind counts, dirty state, and latest commit metadata for attached projects (`src/main/git/gitService.ts:172-250`).
|
||||
- `src/main/sidecar/` resolves, spawns, and communicates with the .NET sidecar while sanitizing the child-process environment (`src/main/sidecar/sidecarProcess.ts:35-232`, `src/main/sidecar/sidecarRuntime.ts:24-48`, `src/main/sidecar/sidecarEnvironment.ts:1-17`).
|
||||
|
||||
### Renderer
|
||||
|
||||
- `src/renderer/App.tsx` is the top-level React composition root. It loads workspace state and sidecar capabilities, subscribes to workspace/session events, applies the theme, and composes `Sidebar`, `ChatPane`, `ActivityPanel`, `WelcomePane`, `SettingsPanel`, and `NewSessionModal` (`src/renderer/App.tsx:83-153`, `src/renderer/App.tsx:240-390`).
|
||||
- The UI is built from focused components under `src/renderer/components/`, with the right-side `ActivityPanel` exposing per-agent activity, run timeline, and per-session tooling toggles (`src/renderer/components/ActivityPanel.tsx:158-311`).
|
||||
- `src/renderer/lib/sessionActivity.ts` maintains ephemeral activity state keyed by session, separate from persisted workspace JSON (`src/renderer/lib/sessionActivity.ts:20-143`).
|
||||
- `src/renderer/styles.css` defines the global visual tokens and light/dark theme switching via CSS custom properties (`src/renderer/styles.css:1-80`).
|
||||
|
||||
### Shared domain and contracts
|
||||
|
||||
- `src/shared/domain/workspace.ts` defines the persisted workspace aggregate (`src/shared/domain/workspace.ts:8-28`).
|
||||
- `src/shared/domain/project.ts` defines project records and guarantees a synthetic scratchpad project with a stable ID (`src/shared/domain/project.ts:34-76`).
|
||||
- `src/shared/domain/session.ts` defines session records, scratchpad-specific config, and session-title resolution (`src/shared/domain/session.ts:27-114`).
|
||||
- `src/shared/domain/pattern.ts` defines orchestration patterns and seeds built-in modes such as `single`, `sequential`, `concurrent`, `handoff`, `group-chat`, and a reserved `magentic` mode (`src/shared/domain/pattern.ts:62-253`).
|
||||
- `src/shared/domain/tooling.ts` defines global tooling settings, per-session tooling selection, and persisted appearance theme settings (`src/shared/domain/tooling.ts:38-95`).
|
||||
- `src/shared/domain/runTimeline.ts` defines persisted per-run history and the event model used to render execution timelines (`src/shared/domain/runTimeline.ts:222-454`).
|
||||
- `src/shared/contracts/ipc.ts` defines the typed renderer API exposed through preload (`src/shared/contracts/ipc.ts:14-99`).
|
||||
- `src/shared/contracts/sidecar.ts` defines the stdio protocol between Electron and the sidecar, including commands, streamed delta events, activity events, and tooling payloads (`src/shared/contracts/sidecar.ts:4-178`).
|
||||
|
||||
### Sidecar
|
||||
|
||||
- `sidecar/src/Eryx.AgentHost/Program.cs` is the .NET entrypoint and requires `--stdio` (`sidecar/src/Eryx.AgentHost/Program.cs:1-10`).
|
||||
- `sidecar/src/Eryx.AgentHost/Services/SidecarProtocolHost.cs` implements the line-delimited JSON protocol. It handles `describe-capabilities`, `validate-pattern`, and `run-turn`, and serializes success/error events back to Electron (`sidecar/src/Eryx.AgentHost/Services/SidecarProtocolHost.cs:24-175`).
|
||||
- `sidecar/src/Eryx.AgentHost/Services/CopilotWorkflowRunner.cs` validates patterns, builds a workflow from the incoming pattern, streams message deltas and agent activity, and returns completed assistant messages (`sidecar/src/Eryx.AgentHost/Services/CopilotWorkflowRunner.cs:31-170`).
|
||||
|
||||
## Main process architecture
|
||||
|
||||
`EryxAppService` is the central application service and the main process entry point for business logic. It owns:
|
||||
|
||||
- workspace loading and caching (`src/main/EryxAppService.ts:99-112`)
|
||||
- project addition and git-context refresh scheduling (`src/main/EryxAppService.ts:104-109`, `src/main/EryxAppService.ts:119-170`, `src/main/EryxAppService.ts:569-581`)
|
||||
- pattern, settings, and tooling mutations (`src/main/EryxAppService.ts:198-314`, `src/main/EryxAppService.ts:477-562`)
|
||||
- session creation, duplication, selection, pin/archive state, and title updates (`src/main/EryxAppService.ts:317-382`, `src/main/EryxAppService.ts:584-601`)
|
||||
- run execution and streamed sidecar integration (`src/main/EryxAppService.ts:385-474`)
|
||||
- persistence and UI fan-out through `workspace-updated` and `session-event` emissions (`src/main/EryxAppService.ts:837-840`)
|
||||
|
||||
This is the main "application boundary" inside Electron: IPC is intentionally thin, and the service concentrates cross-cutting logic instead of scattering it across handlers or renderer components.
|
||||
|
||||
## Persistence model
|
||||
|
||||
Persistent application state is stored as a single JSON document:
|
||||
|
||||
- workspace file path: `app.getPath('userData')/workspace.json` (`src/main/persistence/appPaths.ts:4-6`)
|
||||
- scratchpad directory path: `app.getPath('userData')/scratchpad` (`src/main/persistence/appPaths.ts:8-10`)
|
||||
|
||||
`WorkspaceRepository.load()` normalizes and repairs persisted state on every load:
|
||||
|
||||
- creates the scratchpad directory if needed
|
||||
- seeds the workspace if no JSON exists yet
|
||||
- ensures the scratchpad project is always present
|
||||
- re-merges built-in patterns so shipped defaults stay current
|
||||
- normalizes run records, session tooling, and workspace settings
|
||||
- writes the normalized result back to disk (`src/main/persistence/workspaceRepository.ts:42-77`)
|
||||
|
||||
There is no separate database. The workspace JSON is the durable source of truth for projects, sessions, patterns, settings, and run history.
|
||||
|
||||
## Session and workflow model
|
||||
|
||||
### Projects
|
||||
|
||||
Projects are either:
|
||||
|
||||
- the synthetic scratchpad project with ID `project-scratchpad`
|
||||
- user-added folders with optional git metadata (`src/shared/domain/project.ts:34-76`)
|
||||
|
||||
The scratchpad is treated as a first-class project so the UI can share the same session model across scratchpad and project-backed work.
|
||||
|
||||
### Patterns
|
||||
|
||||
Patterns describe how agents collaborate. Built-in modes currently include:
|
||||
|
||||
- `single`
|
||||
- `sequential`
|
||||
- `concurrent`
|
||||
- `handoff`
|
||||
- `group-chat`
|
||||
- `magentic` (reserved/unavailable in the current .NET runtime) (`src/shared/domain/pattern.ts:62-253`)
|
||||
|
||||
Patterns are shared data, not renderer-only configuration, which lets the same definition drive UI, validation, persistence, and sidecar execution.
|
||||
|
||||
### Sessions
|
||||
|
||||
A `SessionRecord` stores:
|
||||
|
||||
- the selected project and pattern
|
||||
- messages and status
|
||||
- optional scratchpad model overrides
|
||||
- per-session tool selection
|
||||
- persisted run history (`src/shared/domain/session.ts:27-43`)
|
||||
|
||||
Scratchpad sessions can override the primary agent model and reasoning effort, but they cannot enable MCP or LSP tools (`src/main/EryxAppService.ts:477-510`, `src/main/EryxAppService.ts:513-561`).
|
||||
|
||||
### Run timeline
|
||||
|
||||
Every user turn creates a persisted run record before execution starts. That record captures:
|
||||
|
||||
- the triggering user message
|
||||
- pattern mode and participating agents
|
||||
- timestamped events such as thinking, tool calls, handoffs, message progress, completion, and failure (`src/shared/domain/runTimeline.ts:222-454`)
|
||||
|
||||
This allows the UI to show both an activity summary and a historical run timeline instead of only rendering the final assistant text.
|
||||
|
||||
## Renderer architecture
|
||||
|
||||
`App.tsx` is the renderer root and behaves like a thin stateful shell around the shared domain model:
|
||||
|
||||
- loads workspace state and sidecar capabilities on mount (`src/renderer/App.tsx:94-109`)
|
||||
- subscribes to `workspace-updated` and `session-event` streams (`src/renderer/App.tsx:111-132`)
|
||||
- applies the effective theme to the document root (`src/renderer/App.tsx:134-153`)
|
||||
- routes the current state into the main surfaces:
|
||||
- `Sidebar` for projects/sessions
|
||||
- `ChatPane` for the active conversation
|
||||
- `ActivityPanel` for run activity and tooling
|
||||
- `WelcomePane` when no session is selected
|
||||
- `SettingsPanel` and `NewSessionModal` as overlays (`src/renderer/App.tsx:240-390`)
|
||||
|
||||
The renderer does not own persistence. It reacts to snapshots from the main process and sends intents back through the typed preload API.
|
||||
|
||||
## IPC boundary
|
||||
|
||||
The Electron boundary is intentionally explicit:
|
||||
|
||||
- `createMainWindow()` enables `contextIsolation` and disables `nodeIntegration` (`src/main/windows/createMainWindow.ts:26-30`)
|
||||
- the preload script exposes a limited `window.eryxApi` surface (`src/preload/index.ts:6-50`)
|
||||
- the contract for that surface lives in `src/shared/contracts/ipc.ts:14-99`
|
||||
- the main process handles those calls in `src/main/ipc/registerIpcHandlers.ts:24-97`
|
||||
|
||||
That keeps the renderer decoupled from Electron internals and makes IPC shape changes visible in one shared contract file.
|
||||
|
||||
## Sidecar integration
|
||||
|
||||
The main process talks to the sidecar through `SidecarClient`:
|
||||
|
||||
- it resolves the runtime differently for dev and packaged modes (`src/main/sidecar/sidecarRuntime.ts:24-48`)
|
||||
- it spawns the sidecar with a sanitized environment that strips Bun, Copilot, Electron, Node, and npm-prefixed variables (`src/main/sidecar/sidecarEnvironment.ts:1-17`)
|
||||
- it sends JSON commands and reads newline-delimited JSON events from stdout (`src/main/sidecar/sidecarProcess.ts:117-232`)
|
||||
|
||||
The sidecar protocol supports three command types:
|
||||
|
||||
- `describe-capabilities`
|
||||
- `validate-pattern`
|
||||
- `run-turn` (`src/shared/contracts/sidecar.ts:57-79`)
|
||||
|
||||
For `run-turn`, the sidecar streams:
|
||||
|
||||
- `turn-delta` events for partial assistant text
|
||||
- `agent-activity` events for thinking, tool calls, and handoffs
|
||||
- `turn-complete` when the turn is finished (`src/shared/contracts/sidecar.ts:129-178`)
|
||||
|
||||
`EryxAppService.sendSessionMessage()` is the integration point that turns a user message into a run:
|
||||
|
||||
1. append the user message to the session
|
||||
2. mark the session as running
|
||||
3. create a run record
|
||||
4. persist and broadcast the updated workspace
|
||||
5. invoke `sidecar.runTurn(...)`
|
||||
6. apply streamed deltas and activity events as they arrive
|
||||
7. finalize or fail the run and persist again (`src/main/EryxAppService.ts:385-474`)
|
||||
|
||||
## Tooling model
|
||||
|
||||
Tooling is split into two levels:
|
||||
|
||||
- **global definitions** in workspace settings for MCP servers and LSP profiles (`src/shared/domain/tooling.ts:38-48`)
|
||||
- **per-session enablement** through selected MCP/LSP IDs (`src/shared/domain/tooling.ts:50-95`)
|
||||
|
||||
The renderer exposes those toggles in the `ActivityPanel`, but the main process is responsible for validating selections and resolving them into concrete `run-turn` tooling payloads (`src/renderer/components/ActivityPanel.tsx:249-305`, `src/main/EryxAppService.ts:865-943`).
|
||||
|
||||
This gives the app reusable machine-wide tooling definitions while preserving session-level control over which tools are active.
|
||||
|
||||
## Theme and window chrome
|
||||
|
||||
Theme is persisted in workspace settings as `dark`, `light`, or `system` (`src/shared/domain/tooling.ts:43-86`).
|
||||
|
||||
Theme application is split across processes:
|
||||
|
||||
- the renderer sets `data-theme` on the document root and follows OS preference changes for the `system` option (`src/renderer/App.tsx:134-153`)
|
||||
- `src/renderer/styles.css` remaps semantic and Tailwind-backed color tokens for light mode (`src/renderer/styles.css:1-80`)
|
||||
- the main process updates Electron's title bar overlay colors so native window controls stay visually aligned (`src/main/windows/titleBarTheme.ts:20-38`, `src/main/ipc/registerIpcHandlers.ts:38-42`)
|
||||
|
||||
## Activity and git context
|
||||
|
||||
Two pieces of operational state complement the core chat history:
|
||||
|
||||
- **agent activity**, which is ephemeral renderer-side state derived from streamed session events (`src/renderer/lib/sessionActivity.ts:20-143`)
|
||||
- **project git context**, which is durable project metadata refreshed from the local git CLI (`src/main/git/gitService.ts:172-250`, `src/main/EryxAppService.ts:569-581`)
|
||||
|
||||
This combination lets Eryx show both live execution status and repository awareness without overloading the chat transcript itself.
|
||||
|
||||
## Build, test, package, and release architecture
|
||||
|
||||
### Local scripts
|
||||
|
||||
The repository uses Bun as the task runner (`package.json:7-19`):
|
||||
|
||||
- `bun run test` runs TypeScript type-checking and Bun tests
|
||||
- `bun run sidecar:test` runs .NET sidecar tests
|
||||
- `bun run build` builds the Electron app and the .NET sidecar
|
||||
- `bun run package` builds the current platform release bundle
|
||||
|
||||
### Packaging
|
||||
|
||||
Packaging is script-driven rather than delegated to a generic Electron packager:
|
||||
|
||||
- `scripts/releaseTarget.ts` resolves platform- and arch-specific release metadata (`scripts/releaseTarget.ts:7-84`)
|
||||
- `scripts/publish-sidecar.ts` publishes a self-contained .NET sidecar for the current runtime identifier (`scripts/publish-sidecar.ts:32-65`)
|
||||
- `scripts/package-electron.ts` assembles the release directory by copying Electron runtime files, built renderer/main assets, runtime dependencies, and the published sidecar, then applies platform-specific metadata like icons and bundle names (`scripts/package-electron.ts:33-240`)
|
||||
|
||||
### CI and releases
|
||||
|
||||
`.github/workflows/release.yml` defines the pipeline:
|
||||
|
||||
- validate on push and pull request across Windows, macOS, and Linux
|
||||
- create a GitHub release on tag pushes
|
||||
- package each platform in parallel and upload the archives directly to the release (`.github/workflows/release.yml:15-154`)
|
||||
|
||||
## Architectural principles in practice
|
||||
|
||||
The current codebase consistently leans on a few patterns:
|
||||
|
||||
- keep renderer code focused on presentation and user intent
|
||||
- keep persistence and mutation logic in the main process service layer
|
||||
- use shared domain/contracts as the canonical model across boundaries
|
||||
- isolate Copilot and orchestration runtime concerns in the sidecar
|
||||
- make session execution observable through streamed events and persisted run history
|
||||
|
||||
When adding features, prefer extending those boundaries rather than bypassing them.
|
||||
Reference in New Issue
Block a user