diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6ed8a01 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,124 @@ +# Contributing to Hatchling + +How we work in this repo — conventions, structure, and the quality bar. For +what the project *is*, see [`README.md`](README.md); for how it's *designed*, +see [`ARCHITECTURE.md`](ARCHITECTURE.md). + +## Project structure + +``` +hatchling/ +├── src/ +│ ├── main/ Electron main process — Node. DB, providers, IPC, engine. +│ ├── preload/ The context-isolated bridge that exposes window.hatchling. +│ ├── renderer/ React UI. No Node access; talks to main only via the bridge. +│ └── shared/ Types + IPC contract + constants shared by main and renderer. +├── docs/ Deep-dives and backlog (see docs/README.md). +├── build/ Icons and packaging resources. +├── electron.vite.config.ts Three-target build (main / preload / renderer). +├── electron-builder.yml Installer packaging config. +└── ARCHITECTURE.md The spec. The authoritative design document. +``` + +The process boundary is the most important rule in the codebase: + +- **`main`** is trusted. It has Node, the filesystem, the database, and the + network. All LLM calls, all secret handling, all persistence happen here. +- **`renderer`** is untrusted UI. Context isolation is on, node integration is + off. It reaches main *only* through the typed bridge in `preload`. +- **`shared`** is pure types and constants — no Node APIs, no DOM APIs — so it + can be imported from both sides. + +Never widen this boundary (no `nodeIntegration: true`, no exposing `ipcRenderer` +directly, no filesystem access from the renderer). + +## The IPC contract + +Every renderer↔main interaction is typed in [`src/shared/ipc.ts`](src/shared/ipc.ts): + +- `IpcRequestMap` — request/response channels (renderer → main, `invoke`). +- `IpcEventMap` — push events (main → renderer, e.g. streaming tokens). + +To add a channel: add it to the relevant map, implement the handler in main's +handler registry, and call it from the renderer via `window.hatchling.invoke`. +The types flow automatically — a channel can't be called or handled with the +wrong payload. Prefer adding a channel over reaching around the bridge. + +## Provider model + +The app is provider-agnostic. There are exactly **three adapter shapes** plus a +**mock**, all implementing the `LlmProvider` interface in +[`src/shared/types.ts`](src/shared/types.ts): + +- `anthropic` — Anthropic Messages API. +- `openai-compatible` — any OpenAI-shaped endpoint, base-URL configurable + (covers OpenAI, OpenRouter, self-hosted gateways, and Gemini's + OpenAI-compatible endpoint). +- `ollama` — local Ollama HTTP API. +- `mock` — deterministic, offline, no key. **This is the test backbone** — the + whole conversation engine is verified against it without a live model. + +Rules: + +- **No hardcoded model names.** Model lists come from each provider's list + endpoint at runtime. Do not add curated "recommended"/"may struggle" badges. +- **No per-model context-window table.** Use the reported window when available; + fall back to `DEFAULT_CONTEXT_WINDOW` and label it "estimated". +- Adding a real vendor almost never means a new adapter — point the + `openai-compatible` shape at its base URL. + +## Code standards + +- **TypeScript strict, no `any`.** `@typescript-eslint/no-explicit-any` is an + error. Model unknowns with `unknown` and narrow. +- **React**: function components + hooks only. No class components. +- **Styling**: Tailwind utility classes only — no inline styles, no CSS + modules. Theme tokens (`hatch-*` colors) live in `tailwind.config.js`. +- **Async**: wrap I/O and IPC handlers in try/catch and surface a real error to + the user; never swallow. No empty catch blocks. +- **Secrets**: API keys are never logged, never sent to the renderer, never + written to SQLite. They live in the OS keychain via `safeStorage`, keyed by + provider id — or, when no keychain exists, in the app-managed AES-256-GCM + fallback store (see ARCHITECTURE §6). Never weaken the vault so a key becomes + readable by the renderer. +- **Database**: wrap multi-statement writes in transactions. +- **IPC channel names** follow `domain:action` (`providers:list`, + `sessions:create`). +- **Path aliases**: `@shared/*` from anywhere; `@renderer/*` inside the renderer. + +## Testing + +- **Vitest** is the suite. Run `npm test`. +- Main-process and shared logic run in the default `node` environment. +- Renderer component tests opt into a DOM with `// @vitest-environment happy-dom` + at the top of the file. +- **The mock provider is how we test the engine** — tool execution, streaming, + the state machine, file detection, resume — all without a network. A test that + needs a "model" uses the mock, never a real key. +- A bug fix lands with a test that fails before the fix and passes after. + +## The quality gate + +CI runs on every push and PR: **lint · typecheck · test · build**. All four must +be green. Locally, before you push: + +```bash +npm run lint && npm run typecheck && npm test && npm run build +``` + +## Commits & branches + +- Trunk-based: short-lived branches off `master`, merged via PR. +- [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, + `refactor:`, `docs:`, `chore:`, `test:`. Imperative subject, ≤72 chars. +- Update [`CHANGELOG.md`](CHANGELOG.md) under `[Unreleased]` for any + user-visible change. +- **Docs travel with code.** A change that touches a documented surface updates + the doc in the same PR. Stale docs are treated like broken tests. + +## Where things go + +The repo follows a light documentation kata (see [`docs/README.md`](docs/README.md)): +four canonical root files (`README`, `AGENTS`, `ARCHITECTURE`, `CHANGELOG`), +everything else in `docs/`, component notes next to their code, and no fact +written in two places — cross-link instead. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a2a8d4c..cee340d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ > The authoritative design document for Hatchling. Everything else defers to > this file for *how the system is built and why*. For what it is, see -> [`README.md`](README.md); for conventions, see [`CLAUDE.md`](CLAUDE.md). +> [`README.md`](README.md); for conventions, see [`AGENTS.md`](AGENTS.md). --- @@ -298,6 +298,6 @@ Kept out of v1 by design, noted here so the shape doesn't surprise later: ## See also - [`README.md`](README.md) — what the project is and how to run it. -- [`CLAUDE.md`](CLAUDE.md) — conventions and workflow. +- [`AGENTS.md`](AGENTS.md) — conventions and workflow. - [`src/shared/`](src/shared) — the types, IPC contract, and constants this document describes. diff --git a/CLAUDE.md b/CLAUDE.md index 160605a..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,124 +1 @@ -# Contributing to Hatchling - -How we work in this repo — conventions, structure, and the quality bar. For -what the project *is*, see [`README.md`](README.md); for how it's *designed*, -see [`ARCHITECTURE.md`](ARCHITECTURE.md). - -## Project structure - -``` -hatchling/ -├── src/ -│ ├── main/ Electron main process — Node. DB, providers, IPC, engine. -│ ├── preload/ The context-isolated bridge that exposes window.hatchling. -│ ├── renderer/ React UI. No Node access; talks to main only via the bridge. -│ └── shared/ Types + IPC contract + constants shared by main and renderer. -├── docs/ Deep-dives and backlog (see docs/README.md). -├── build/ Icons and packaging resources. -├── electron.vite.config.ts Three-target build (main / preload / renderer). -├── electron-builder.yml Installer packaging config. -└── ARCHITECTURE.md The spec. The authoritative design document. -``` - -The process boundary is the most important rule in the codebase: - -- **`main`** is trusted. It has Node, the filesystem, the database, and the - network. All LLM calls, all secret handling, all persistence happen here. -- **`renderer`** is untrusted UI. Context isolation is on, node integration is - off. It reaches main *only* through the typed bridge in `preload`. -- **`shared`** is pure types and constants — no Node APIs, no DOM APIs — so it - can be imported from both sides. - -Never widen this boundary (no `nodeIntegration: true`, no exposing `ipcRenderer` -directly, no filesystem access from the renderer). - -## The IPC contract - -Every renderer↔main interaction is typed in [`src/shared/ipc.ts`](src/shared/ipc.ts): - -- `IpcRequestMap` — request/response channels (renderer → main, `invoke`). -- `IpcEventMap` — push events (main → renderer, e.g. streaming tokens). - -To add a channel: add it to the relevant map, implement the handler in main's -handler registry, and call it from the renderer via `window.hatchling.invoke`. -The types flow automatically — a channel can't be called or handled with the -wrong payload. Prefer adding a channel over reaching around the bridge. - -## Provider model - -The app is provider-agnostic. There are exactly **three adapter shapes** plus a -**mock**, all implementing the `LlmProvider` interface in -[`src/shared/types.ts`](src/shared/types.ts): - -- `anthropic` — Anthropic Messages API. -- `openai-compatible` — any OpenAI-shaped endpoint, base-URL configurable - (covers OpenAI, OpenRouter, self-hosted gateways, and Gemini's - OpenAI-compatible endpoint). -- `ollama` — local Ollama HTTP API. -- `mock` — deterministic, offline, no key. **This is the test backbone** — the - whole conversation engine is verified against it without a live model. - -Rules: - -- **No hardcoded model names.** Model lists come from each provider's list - endpoint at runtime. Do not add curated "recommended"/"may struggle" badges. -- **No per-model context-window table.** Use the reported window when available; - fall back to `DEFAULT_CONTEXT_WINDOW` and label it "estimated". -- Adding a real vendor almost never means a new adapter — point the - `openai-compatible` shape at its base URL. - -## Code standards - -- **TypeScript strict, no `any`.** `@typescript-eslint/no-explicit-any` is an - error. Model unknowns with `unknown` and narrow. -- **React**: function components + hooks only. No class components. -- **Styling**: Tailwind utility classes only — no inline styles, no CSS - modules. Theme tokens (`hatch-*` colors) live in `tailwind.config.js`. -- **Async**: wrap I/O and IPC handlers in try/catch and surface a real error to - the user; never swallow. No empty catch blocks. -- **Secrets**: API keys are never logged, never sent to the renderer, never - written to SQLite. They live in the OS keychain via `safeStorage`, keyed by - provider id — or, when no keychain exists, in the app-managed AES-256-GCM - fallback store (see ARCHITECTURE §6). Never weaken the vault so a key becomes - readable by the renderer. -- **Database**: wrap multi-statement writes in transactions. -- **IPC channel names** follow `domain:action` (`providers:list`, - `sessions:create`). -- **Path aliases**: `@shared/*` from anywhere; `@renderer/*` inside the renderer. - -## Testing - -- **Vitest** is the suite. Run `npm test`. -- Main-process and shared logic run in the default `node` environment. -- Renderer component tests opt into a DOM with `// @vitest-environment happy-dom` - at the top of the file. -- **The mock provider is how we test the engine** — tool execution, streaming, - the state machine, file detection, resume — all without a network. A test that - needs a "model" uses the mock, never a real key. -- A bug fix lands with a test that fails before the fix and passes after. - -## The quality gate - -CI runs on every push and PR: **lint · typecheck · test · build**. All four must -be green. Locally, before you push: - -```bash -npm run lint && npm run typecheck && npm test && npm run build -``` - -## Commits & branches - -- Trunk-based: short-lived branches off `master`, merged via PR. -- [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, - `refactor:`, `docs:`, `chore:`, `test:`. Imperative subject, ≤72 chars. -- Update [`CHANGELOG.md`](CHANGELOG.md) under `[Unreleased]` for any - user-visible change. -- **Docs travel with code.** A change that touches a documented surface updates - the doc in the same PR. Stale docs are treated like broken tests. - -## Where things go - -The repo follows a light documentation kata (see [`docs/README.md`](docs/README.md)): -four canonical root files (`README`, `CLAUDE`, `ARCHITECTURE`, `CHANGELOG`), -everything else in `docs/`, component notes next to their code, and no fact -written in two places — cross-link instead. +@AGENTS.md diff --git a/README.md b/README.md index e5d9a00..5ebd019 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ Installers are written to `release/`. | `npm run test:coverage` | Tests with coverage | | `npm run package` | Build installers for the current platform | -See [`CLAUDE.md`](CLAUDE.md) for conventions and [`ARCHITECTURE.md`](ARCHITECTURE.md) +See [`AGENTS.md`](AGENTS.md) for conventions and [`ARCHITECTURE.md`](ARCHITECTURE.md) for the system design. ## Tech stack diff --git a/docs/README.md b/docs/README.md index ddcb562..1c79fe7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ > that don't belong in one of the four canonical root files live here. > > Authoritative pointers: [`ARCHITECTURE.md`](../ARCHITECTURE.md) (the spec / -> rules), [`README.md`](../README.md) (what exists), [`CLAUDE.md`](../CLAUDE.md) +> rules), [`README.md`](../README.md) (what exists), [`AGENTS.md`](../AGENTS.md) > (how we work). This file is the map; those are the sources of truth. --- @@ -25,7 +25,7 @@ opens with a one-line "what this is" and closes with a "see also". This repo follows a lightweight documentation kata: -1. The root holds only canonical entry points: `README.md`, `CLAUDE.md`, +1. The root holds only canonical entry points: `README.md`, `AGENTS.md`, `ARCHITECTURE.md`, `CHANGELOG.md`. 2. `docs/` holds every deep-dive. 3. Component-specific docs live next to their code. @@ -38,4 +38,4 @@ This repo follows a lightweight documentation kata: - [`../ARCHITECTURE.md`](../ARCHITECTURE.md) — system design and decisions. - [`../README.md`](../README.md) — what the project is and how to run it. -- [`../CLAUDE.md`](../CLAUDE.md) — conventions and workflow. +- [`../AGENTS.md`](../AGENTS.md) — conventions and workflow.