diff --git a/.gitignore b/.gitignore index 3176707..d203a24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target plans -sum* +/sum* Cargo.toml.orig diff --git a/AGENTS.md b/AGENTS.md index 1eb1c71..d6cc08a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`. @@ -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 ` to create one, `/profile` to switch, and @@ -53,7 +58,9 @@ settings for future launches; manual settings changes clear its active-profile marker. Profiles are stored separately in `/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 @@ -65,7 +72,8 @@ 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 @@ -73,28 +81,40 @@ 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/`. @@ -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. @@ -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`. @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea9b95..9050e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 5b50479..0e5945c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1346,9 +1346,6 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tracing", - "tracing-appender", - "tracing-subscriber", ] [[package]] @@ -1763,7 +1760,6 @@ name = "providers" version = "0.1.0" dependencies = [ "async-trait", - "futures-util", "llm", "reqwest", "serde", diff --git a/README.md b/README.md index fc80da1..55a9447 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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 ` | 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 ` | Pin a custom provider for the current OpenRouter model | +| `/model-providers ` | Pin a custom provider for the current OpenRouter model | | `/help` | List available commands | +| `/quit` | Exit Alan | ## Permissions @@ -115,9 +117,10 @@ A single tool call may chain several commands (`cargo test && cargo run`, or wit Grants persist per-project at `/projects//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 @@ -127,7 +130,7 @@ 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 @@ -135,14 +138,38 @@ 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 diff --git a/crates/alan/Cargo.toml b/crates/alan/Cargo.toml index d2a6529..85e3aef 100644 --- a/crates/alan/Cargo.toml +++ b/crates/alan/Cargo.toml @@ -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]] diff --git a/crates/llm/Cargo.toml b/crates/llm/Cargo.toml index 51ab93a..dc20085 100644 --- a/crates/llm/Cargo.toml +++ b/crates/llm/Cargo.toml @@ -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 } diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index e31c6b8..8936cf2 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -13,5 +13,4 @@ reqwest = { workspace = true } tokio = { workspace = true, features = ["sync", "rt"] } [dev-dependencies] -futures-util = { workspace = true } tokio = { workspace = true } diff --git a/crates/tui/examples/greet.rs b/crates/tui/examples/greet.rs index d3d3037..7775200 100644 --- a/crates/tui/examples/greet.rs +++ b/crates/tui/examples/greet.rs @@ -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; @@ -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());