Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand Down Expand Up @@ -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.
125 changes: 1 addition & 124 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand All @@ -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.
Expand All @@ -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.
Loading