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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/target
plans
sum*
/sum*
Cargo.toml.orig
72 changes: 39 additions & 33 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

## Project

Alan is minimal coding-agent written in rust.
Alan is a minimal coding agent written in Rust.

Workspace crates:

- `crates/llm/` — provider-independent LLM protocol types and API clients.
- `crates/providers/` — provider/model binding and authentication.
- `crates/tools/` — built-in file system and shell tools.
- `crates/agent/` — conversation state, tool calls, system prompt, agent loop.
- `crates/tui/` — package `alan-tui`, a standalone Ratatui runtime: components,
event loop, terminal lifecycle. Published to crates.io.
- `crates/alan/` — interactive ratatui REPL frontend.

Workspace manifest: `Cargo.toml`.
Expand Down Expand Up @@ -36,15 +39,17 @@ cargo check -p alan
cargo test -p agent
cargo test -p llm
cargo test -p providers
cargo test -p tools
cargo test -p alan-tui
```

Run formatting and checks after Rust code changes. Do not run release or destructive Git commands unless requested.

### Model selection

`/models` opens a picker backed by the OpenRouter model catalog. The catalog
is fetched on first use and cached; selecting a model switches the active
conversation model and updates the session header.
`/models` opens a picker over the model catalogs of every registered provider.
Selecting a model switches the active conversation model and updates the
session header.

Profiles save the current provider/model, reasoning effort, and web-fetch/search
settings. Use `/profile save <name>` to create one, `/profile` to switch, and
Expand All @@ -53,7 +58,9 @@ settings for future launches; manual settings changes clear its active-profile
marker. Profiles are stored separately in `<data dir>/profiles.json`. See
`core::paths` for how the data directory is resolved.

Alan currently uses OpenRouter:
`build_providers` in `crates/alan/src/main.rs` lists the registered providers.
The `providers` crate also has a Google provider that is not registered. Sign in
with `/login`, or set the provider's API key variable:

```bash
OPENROUTER_API_KEY=... cargo run -p alan
Expand All @@ -65,36 +72,49 @@ Optional model override:
ALAN_MODEL=openai/gpt-4o-mini cargo run -p alan
```

Current default model is `openai/gpt-4o-mini`.
With no saved settings, Alan uses the `openrouter` provider and
`DEFAULT_MODEL` from `crates/alan/src/core/settings.rs`.

## Architecture

Keep dependency direction one-way:

```text
llm <- providers <- agent <- alan
llm <- tools <- agent
alan-tui <- alan
```

`llm` must not depend on providers, agent, or UI.

`agent` must not depend on ratatui or crossterm.

`alan` owns terminal setup, key mapping, and ratatui rendering.
`alan-tui` must not depend on other workspace crates.

`alan-tui` owns terminal setup and the event loop. `alan` owns key mapping and
ratatui rendering.

### Alan frontend boundary

```text
crates/alan/src/main.rs
terminal lifecycle and crossterm event polling
provider setup, model selection, runtime launch

crates/alan/src/keymap.rs
crossterm events to `AlanAction`

crates/alan/src/root.rs
root component: routes `AlanAction` to children, lays out the screen

crates/alan/src/core/
UI-independent actions, controller, transcript, agent execution
UI-independent chat controller, transcript, commands, settings,
permissions, skills

crates/alan/src/views/
ratatui adapter, UI state, layout, rendering
ratatui components and theme
```

`core` must stay frontend-independent. Future GPUI frontend should reuse `Controller`, `Entry`, and `Action`, then provide its own event mapping and renderer.
`core` must stay frontend-independent. A future GPUI frontend should reuse `ChatController` and `Entry`, then provide its own event mapping and renderer.

Do not add ratatui/crossterm types to `crates/alan/src/core/`.

Expand All @@ -111,7 +131,7 @@ auto-includes it in release archives.

## Current Alan UI

`crates/alan/src/views/mod.rs` provides minimal borderless ratatui UI:
`crates/alan/src/views/components/` provides minimal borderless ratatui UI:

- Header.
- Scrollable chat history.
Expand All @@ -121,27 +141,15 @@ auto-includes it in release archives.
- Status line above editor.
- Input cursor placement.

`UiState` owns frontend interaction state:

- input text
- transcript scroll offset
- auto-follow output state

`Action` is UI-independent input intent:

- `Quit`
- `Submit`
- `ClearInput`
- `Backspace`
- `Insert(char)`
- `ScrollUp`
- `ScrollDown`
`ChatView` (`views/components/chat_view/`) owns the chat session: the
`ChatController` and the agent stream. `PromptEditor` owns input. Overlays
such as login and local models live beside `root.rs`.

Keep UI simple. Avoid borders, unnecessary widgets, and premature abstraction.

## Current Agent Behavior

`crates/agent/src/agent.rs` currently:
`crates/agent/src/agent/` currently:

- Stores conversation history in memory.
- Sends prompts through bound `providers::Model`.
Expand Down Expand Up @@ -198,11 +206,9 @@ skip is `tracing::warn!`-logged.
## UI Rules

- No core dependency on terminal framework.
- Frontend converts native events into `Action`.
- Frontend converts native events into `AlanAction`.
- Renderer reads state; it should not perform network calls.
- Controller owns agent interaction and transcript state.
- Keep visual constants centralized in view module.
- `ChatController` owns agent interaction and transcript state.
- Keep visual constants centralized in `views/theme.rs`.
- Use terminal display width for cursor/layout calculations, not byte count.
- Keep auto-scroll behavior explicit.

Implement in this order unless user requests different scope. Keep each step usable.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Popup v2 clears before rendering a message such that background text doesn't overlap with popup's message.
- Text selection in the chat history is precise now.

## 0.1.4 - 2026-10-4
## 0.1.4 - 2026-10-04

### Added

Expand Down
4 changes: 0 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 37 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ A minimal coding agent in your terminal.

## Features

- **First-class OpenRouter support** — full model catalog, per-model custom provider pinning (`/model-provider`), and reasoning effort control (`/effort`).
- **First-class OpenRouter support** — full model catalog, per-model custom provider pinning (`/model-providers`), and reasoning effort control (`/effort`).
- **Paste images** — drop a screenshot from your clipboard straight into the conversation and ask about it.
- **Compact with focus** — `/summarize-new "focus hint"` summarizes the context and restarts with just what matters.
- **Steering** — queue a message mid-run and the agent picks it up without waiting for the response to finish.
Expand Down Expand Up @@ -75,10 +75,10 @@ Alan currently supports:

Use the `/login` command to sign in to any of the available providers.

For OpenRouter, you can pin a custom provider for the selected model with `/model-provider`, followed by the provider name (no quotes):
For OpenRouter, you can pin a custom provider for the selected model with `/model-providers`, followed by the provider name (no quotes):

- `/model-provider deepseek`
- `/model-provider xiaomi/fp8`
- `/model-providers deepseek`
- `/model-providers xiaomi/fp8`

## Usage

Expand All @@ -96,12 +96,14 @@ For OpenRouter, you can pin a custom provider for the selected model with `/mode
| `/summarize-new [focus]` | Summarize the context and restart; optionally pass a quoted focus hint, e.g. `/summarize-new "just take the XYZ details"` |
| `/effort` | Set reasoning effort (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) |
| `/fork` | Fork the session from a checkpoint |
| `/rename <name>` | Rename the current session |
| `/tool-free` | Allow all tool calls without asking |
| `/tool-slip` | Approve a command family once, allow its siblings |
| `/tool-strict` | Allow only exact commands already approved |
| `/local` | Manage local models (add, remove, edit) |
| `/model-provider <name>` | Pin a custom provider for the current OpenRouter model |
| `/model-providers <name>` | Pin a custom provider for the current OpenRouter model |
| `/help` | List available commands |
| `/quit` | Exit Alan |

## Permissions

Expand All @@ -115,9 +117,10 @@ A single tool call may chain several commands (`cargo test && cargo run`, or wit

Grants persist per-project at `<data dir>/projects/<pwd-hash>/permissions.json`. The current policy is shown as a glyph in the status line.

Alan starts in strict policy. Switch with:
Alan starts in strict policy and remembers the policy you pick across runs. Switch with:
- `/tool-slip`
- `/tool-free`
- `/tool-strict`

## Local models

Expand All @@ -127,22 +130,46 @@ Alan supports OpenAI-compatible local models.
/local add
```

This opens an overlay with fields for Model ID, URL, API, and API Key. Use `/local remove` and `/local edit` to manage your local models. No API key or `/login` is needed for local models.
This opens an overlay with fields for URL, API, API Key, and Model ID. Alan queries the server's `/models` endpoint and offers the detected models in a searchable picker. Use `/local remove` and `/local edit` to manage your local models. No API key or `/login` is needed for local models.

## Configuration and data locations

Alan stores everything under `~/.alan/` by default:

- `settings.json` — your current settings and active-profile marker.
- `profiles.json` — saved named model/reasoning/web settings profiles.
- `auth.json` — provider credentials saved by `/login`.
- `local_models.json` — local models added with `/local`.
- `projects/` — per-project permission grants.
- `sessions/` — conversation history (append-only JSONL, one file per session).
- `skills/` — personal skills, available in every project.
- `logs/` — daily rotating logs.

All of these live in the data directory (`~/.alan` by default). `ALAN_HOME` selects the *parent* of that directory, so `ALAN_HOME=/tmp/x` puts everything in `/tmp/x/.alan`. Two other environment variables are useful:
All of these live in the data directory (`~/.alan` by default).

- `ALAN_LOG_DIR` — override the log directory.
- `ALAN_MODEL` — override the default model on startup, e.g. `ALAN_MODEL=openai/gpt-4o-mini alan`.
### Environment variables

Settings variables override `settings.json` for that run without changing it. Start Alan with `--save` to write them into `settings.json`.

| Variable | Effect |
| --- | --- |
| `ALAN_HOME` | Parent of the data directory: `ALAN_HOME=/tmp/x` puts everything in `/tmp/x/.alan`. |
| `ALAN_PROVIDER` | Provider id: `openrouter`, `deepseek`, `zai`, or `local`. |
| `ALAN_MODEL` | Model id for that provider, e.g. `ALAN_MODEL=openai/gpt-4o-mini alan`. |
| `ALAN_REASONING_EFFORT` | `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. |
| `ALAN_OPENROUTER_WEB_FETCH` | Turn OpenRouter's web fetch tool on or off: `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`. |
| `ALAN_OPENROUTER_WEB_SEARCH` | Turn OpenRouter's web search tool on or off, same values. |
| `ALAN_OR_MODEL_PROVIDER` | Comma-separated OpenRouter provider order for the selected model, e.g. `deepseek,xiaomi/fp8`. |
| `ALAN_SESSION` | Resume a saved session by id. Alan prints the exact command when it exits. |
| `ALAN_LOG` | Log filter in `tracing` `EnvFilter` syntax. Falls back to `RUST_LOG`. |
| `ALAN_LOG_DIR` | Log directory, default `~/.alan/logs`. |
| `OPENROUTER_API_KEY`, `DEEPSEEK_API_KEY`, `ZAI_API_KEY` | API key for that provider, used when `/login` has not saved one. |

### Flags

- `--save` — write the settings environment variables above into `settings.json`.
- `--blank` — start without Alan's default system prompt.
- `--version` — print the installed version.

## Skills

Expand Down
4 changes: 1 addition & 3 deletions crates/alan/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ tracing-subscriber = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
ignore = { workspace = true }
fs2 = "0.4"
fs2 = { workspace = true }

[dev-dependencies]
llm = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true, features = ["test-util"] }

[[bin]]
Expand Down
3 changes: 0 additions & 3 deletions crates/llm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,3 @@ serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
1 change: 0 additions & 1 deletion crates/providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ reqwest = { workspace = true }
tokio = { workspace = true, features = ["sync", "rt"] }

[dev-dependencies]
futures-util = { workspace = true }
tokio = { workspace = true }
4 changes: 2 additions & 2 deletions crates/tui/examples/greet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! greets you and echoes what you typed.
//!
//! The editor is configured the same way alan configures its prompt
//! (`UiState::new_editor`): word wrap, hidden in-widget cursor, and the real
//! (`PromptEditor::new_editor`): word wrap, hidden in-widget cursor, and the real
//! terminal cursor positioned from `rendered_cursor_position`.

use std::time::Duration;
Expand All @@ -24,7 +24,7 @@ struct Greet {
}

impl Greet {
/// Same configuration as alan's `UiState::new_editor`.
/// Same configuration as alan's `PromptEditor::new_editor`.
fn new_editor() -> TextArea<'static> {
let mut editor = TextArea::default();
editor.set_style(Style::default());
Expand Down
Loading