diff --git a/AGENTS.md b/AGENTS.md index 1b3359f..305fc95 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,7 +169,7 @@ [providers.ollama_cloud] enabled = true base_url = "https://api.ollama.com" # [Inference] default - api_key = "" # read from env OLLEN_OLLAMA_CLOUD_API_KEY if empty + api_key = "" # read from env OWLEN_OLLAMA_CLOUD_API_KEY if empty hourly_quota_tokens = 50000 # configurable; visual only weekly_quota_tokens = 250000 # configurable; visual only list_ttl_secs = 60 diff --git a/agents-2025-10-23.md b/agents-2025-10-23.md new file mode 100644 index 0000000..ca9482c --- /dev/null +++ b/agents-2025-10-23.md @@ -0,0 +1,90 @@ +## fix(config): align ollama cloud defaults with api host +- **Severity:** High – cloud calls 404 because defaults still point at `https://ollama.com` and require `OLLAMA_CLOUD_API_KEY` (`crates/owlen-core/src/config.rs:26`, `config.toml:16`, `README.md:6`). +- **Issue:** Fresh installs silently fail against Ollama Cloud; env naming diverges from the spec (`OWLEN_OLLAMA_CLOUD_API_KEY` vs vendor-provided `OLLAMA_API_KEY`) so doctor tooling cannot guide users. +- **Plan:** Switch canonical host to `https://api.ollama.com`, teach config loaders to honor the new env while warning on the legacy name, and update generated configs, doctor output, and docs. +- **Acceptance Criteria:** New configs hit cloud without manual overrides; legacy env keeps working with a single warning; `owlen providers status` shows cloud reachable when the key is set. +- **Test Notes:** Extend `Config` env resolution tests, add CLI smoke test for status under both env names, and run `cargo test -p owlen-core -p owlen-cli`. + +## fix(provider/ollama): keep stream whitespace intact +- **Severity:** High – `generate_stream` trims every line so assistants lose leading newlines and indentation (`crates/owlen-providers/src/ollama/shared.rs:135` and :161). +- **Issue:** Markdown blocks and code fences arrive malformed, forcing the UI to patch formatting after the fact. +- **Plan:** Replace the naive `trim()` with CR/LF stripping that preserves user-visible whitespace, keep incomplete line fragments buffered, and add defensive logging around JSON decode failures. +- **Acceptance Criteria:** Streaming chunks can start with whitespace (code blocks render correctly); end-of-stream metadata still lands in the final message; no regressions for local or cloud daemons. +- **Test Notes:** Add unit tests that feed `\r\n`-terminated chunks with leading spaces, plus an integration test using recorded `samples.json`; run `cargo test -p owlen-providers`. + +## feat(provider/ollama): wire tool calling and response metadata +- **Severity:** High – Ollama models never see tool descriptors because `prepare_chat_request` drops them and `supports_tools` is hard-coded false (`crates/owlen-core/src/providers/ollama.rs:825` and :1258). +- **Issue:** The agent pipeline cannot hand off `web.search` or other tools; chat stalls when the model requests a function call. +- **Plan:** Send tool schemas to Ollama, surface tool_call deltas back into `ChatResponse`, mark capable models as tool-ready, and ensure session handling replays tool results before resuming generation. +- **Acceptance Criteria:** Models that emit tool calls loop through the MCP tool registry without manual intervention; disabling tools blanks them out; streaming transcripts keep tool boundaries. +- **Test Notes:** Add provider tests covering `tool_calls` frames, extend session streaming tests with mock tool responses, and run `cargo test -p owlen-core`. + +## feat(usage): track cloud token quotas and expose :limits +- **Severity:** Medium – there is no hourly/weekly accounting or `:limits` command even though the UI promises it (`crates/owlen-core/src/session.rs`, `crates/owlen-tui/src/chat_app.rs:6880` area, `crates/owlen-tui/src/ui.rs:2280` area). +- **Issue:** Users cannot see or cap consumption, and the header lacks the cloud usage line from the spec. +- **Plan:** Introduce a persisted usage ledger (rolling hour/week windows) keyed by provider, surface quotas from config, add `:limits` to the palette, and render an extra header row with colored percentages plus 80%/95% toasts. +- **Acceptance Criteria:** Usage survives restart, `:limits` prints hourly and weekly tallies, header updates after each completion, and quota breaches raise a toast once per band. +- **Test Notes:** Write time-travel unit tests for the tracker, snapshot the new header rendering, add a TUI command test, and run `cargo test -p owlen-core -p owlen-tui`. + +## feat(tool/web): route searches through provider and gate on cloud availability +- **Severity:** High – `web_search` hits DuckDuckGo directly (`crates/owlen-core/src/tools/web_search.rs:14`) and the registry exposes it even when the cloud provider is disabled (`crates/owlen-core/src/session.rs:245` range). +- **Issue:** In restricted environments the tool fails silently, and the model keeps saying it lacks internet access despite the stated goal. +- **Plan:** Replace the DuckDuckGo client with a provider-aware adapter that calls the configured cloud search endpoint, hide the tool unless the active provider advertises web search support, and surface clean errors for 401/429 responses. +- **Acceptance Criteria:** With a valid cloud key the assistant auto-invokes `web.search`; without cloud access the tool is absent and the model does not ask for it; detailed errors reach the toast system. +- **Test Notes:** Add Wiremock-backed integration tests for happy path, 401, and 429; assert tool registration toggles via session tests; run `cargo test -p owlen-core --tests tools`. + +## feat(ui): modernize chat chrome and usage surfacing +- **Severity:** Medium – the current TUI still uses heavy bordered blocks and text badges (`crates/owlen-tui/src/ui.rs:200` onward) and lacks the cockpit-style progress bars requested for context and usage. +- **Issue:** Visual hierarchy is dated, the context badge is text-only, and screenshots (`images/layout.png`) no longer match the roadmap. +- **Plan:** Refresh the header with gradient bars for context and cloud usage, adopt lighter glassmorphism-inspired panels, tighten spacing, update themes, and regenerate screenshots for docs. +- **Acceptance Criteria:** Header shows context and usage bars with color thresholds, panels use the new styling without breaking vim keybinds, and updated screenshots appear in docs. +- **Test Notes:** Update golden snapshot tests for the layout, add UI smoke tests for narrow terminals, and capture new PNGs via the existing screenshot script. + +## docs(release): prep v0.2 guidance and config samples +- **Severity:** Medium – public docs still advertise v0.1.11 and outdated setup steps (`README.md:6`, `docs/troubleshooting.md`, `AGENTS.md` samples out of sync with actual config schema). +- **Issue:** Users follow stale instructions, miss new keys (hourly/weekly quotas), and cannot reconcile the config example with generated files. +- **Plan:** Bump version strings to v0.2, document the new config sections (quotas, list TTL), add troubleshooting for cloud auth and rate limits, and note the revised env naming. +- **Acceptance Criteria:** README, docs, and sample configs match the implemented features; upgrade notes explain migration steps; release checklist reflects the new DoD. +- **Test Notes:** Run link checker over `docs/`, lint Markdown with `cargo xtask lint-docs`, and have CI confirm the changelog entry. + +## feat(commands): expose `:web on|off` toggle in both TUI and CLI +- **Severity:** Medium – there is no runtime switch for the web-search tool (`crates/owlen-tui/src/commands/registry.rs`, `crates/owlen-tui/src/chat_app.rs`), so users must hand-edit config to disable remote lookups when on metered links. +- **Issue:** Lack of a fast toggle contradicts the DoD requirement for obvious tooling controls and encourages unsafe defaults when privacy-sensitive users need a local-only session. +- **Plan:** Add `:web on|off` and `owlen providers web --enable/--disable` commands that mutate `config.tools.web_search.enabled`, broadcast the change to active sessions, and synchronise the header indicator/toast copy. +- **Acceptance Criteria:** Toggling via command takes effect immediately (tool descriptors disappear/appear for the next request), the setting persists to disk, and both CLI+TUI help list the new flags. +- **Test Notes:** Extend command registry unit tests, add an integration test that toggles and asserts tool exposure, and validate persistence with a temporary config dir. + +## feat(config): align generated config with provider sections & env fallbacks +- **Severity:** Medium – the sample `config.toml` and `README` snippets still use flat `[providers.ollama_cloud]` keys with `api_key_env = "OLLAMA_CLOUD_API_KEY"` and base URL `https://ollama.com` (`config.toml:16`, `README.md:119`), diverging from `crates/owlen-core/src/config.rs`. +- **Issue:** Onboarding breaks because the UI and doctor expect the `[providers.ollama_cloud]` schema with `OWLEN_OLLAMA_CLOUD_API_KEY` and `[providers.ollama]` defaults, so fresh installs require manual correction. +- **Plan:** Regenerate the CLI templated config, update documentation snippets, implement migration warnings for the legacy env name, and ensure doctor explains both env vars with precedence. +- **Acceptance Criteria:** `owlen config init` writes the new schema, doctor reports actionable guidance when the legacy env is present, and README/config docs match the generated file. +- **Test Notes:** Snapshot regeneration output, add config migration tests covering legacy envs, and run `cargo test -p owlen-core config` suite. + +## refactor(errors): typed provider failures mapped to structured toasts +- **Severity:** High – the TUI currently inspects error strings for `"429"` / `"rate limit"` substrings (`crates/owlen-tui/src/chat_app.rs:10377`) and loses context when providers change wording. +- **Issue:** String matching misses localized messages and risks leaking raw responses into the UI, preventing reliable recovery and localisation. +- **Plan:** Introduce a typed error enum in `owlen-core` (`Error::Unauthorized`, `::RateLimited`, `::Unavailable`, etc.), propagate it through providers, and map the variants to toast/banners with deterministic copy. +- **Acceptance Criteria:** Cloud 401/429 cases surface the new typed variants, the UI no longer string-parses errors, logs redact secrets, and analytics tests assert the mapping. +- **Test Notes:** Extend provider unit tests for HTTP status translation, add UI tests that inject each error variant, and fuzz unknown error text to ensure graceful fallback. + +## test(integration): cover local/cloud/search tool happy and unhappy paths +- **Severity:** Medium – there are no Wiremock-backed integration cases validating multi-provider flows; regressions slip through (`crates/owlen-core/tests` lacks HTTP stubs). +- **Issue:** Streaming/tool-call regressions and auth handling bugs reach users because only unit tests exist. +- **Plan:** Create integration tests that spin up stub servers for `/api/tags`, `/api/chat`, and `/v1/web/search`, covering 200/401/429 responses, tool-call loops, and rate-limit recovery. +- **Acceptance Criteria:** CI runs the new suite, failures are deterministic, and each scenario asserts provider status transitions plus persisted usage updates. +- **Test Notes:** Use `wiremock` for HTTP stubbing, reuse sample transcripts from `samples.json`, and document the fixtures under `tests/fixtures/`. + +## chore(deps/ui): upgrade to Ratatui 0.29+ and enable gradient widgets +- **Severity:** Medium – we pin `ratatui = 0.28` and `crossterm = 0.28` (`Cargo.toml`), missing gradient fills, flexible layout APIs, and native mouse handling released in 2025. +- **Issue:** Without the newer widgets we hand-roll gradients/badges and duplicate layout math, increasing maintenance debt. +- **Plan:** Bump Ratatui/Crossterm, adopt the `Gauge` gradient support for context/usage bars, replace custom layout helpers with the new `Flex` API, and ensure theme palettes cover extended colour spaces. +- **Acceptance Criteria:** Build succeeds with the upgraded stack, header widgets use the new gradient gauges, and CI snapshots reflect the refreshed rendering. +- **Test Notes:** Run `cargo test -p owlen-tui`, update golden snapshots, and manually verify mouse/resize handling in the demo app. + +## chore(release): bump workspace metadata to v0.2 +- **Severity:** Medium – the workspace version, crates, and README badges still say 0.1.11 (`Cargo.toml`, `README.md:8`), undermining the v0.2 delivery narrative. +- **Issue:** Packaging scripts (AUR, PKGBUILD) and CLI `--version` output disagree with the roadmap, confusing users and automated installers. +- **Plan:** Update `[workspace.package].version`, propagate the bump to crate manifests, adjust badges/screenshots, and refresh packaging metadata (PKGBUILD, docs/CHANGELOG). +- **Acceptance Criteria:** `owlen --version` prints 0.2.0, README badges align, and release notes enumerate the v0.2 features. +- **Test Notes:** Run `cargo xtask lint-release`, build the Arch package locally, and smoke-test binary version output. diff --git a/crates/owlen-core/src/config.rs b/crates/owlen-core/src/config.rs index 8101cfa..628cea7 100644 --- a/crates/owlen-core/src/config.rs +++ b/crates/owlen-core/src/config.rs @@ -23,8 +23,14 @@ pub const OLLAMA_MODE_KEY: &str = "ollama_mode"; /// Extra config key storing the preferred Ollama Cloud endpoint. pub const OLLAMA_CLOUD_ENDPOINT_KEY: &str = "cloud_endpoint"; /// Canonical Ollama Cloud base URL. -pub const OLLAMA_CLOUD_BASE_URL: &str = "https://ollama.com"; -/// Environment variable used for Ollama Cloud authentication. +pub const OLLAMA_CLOUD_BASE_URL: &str = "https://api.ollama.com"; +/// Legacy Ollama Cloud base URL (accepted for backward compatibility). +pub const LEGACY_OLLAMA_CLOUD_BASE_URL: &str = "https://ollama.com"; +/// Preferred environment variable used for Ollama Cloud authentication. +pub const OWLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OWLEN_OLLAMA_CLOUD_API_KEY"; +/// Legacy environment variable accepted for backward compatibility. +pub const LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLEN_OLLAMA_CLOUD_API_KEY"; +/// Legacy environment variable still accepted for Ollama Cloud authentication. pub const OLLAMA_CLOUD_API_KEY_ENV: &str = "OLLAMA_CLOUD_API_KEY"; /// Default base URL for local Ollama daemons. pub const OLLAMA_LOCAL_BASE_URL: &str = "http://localhost:11434"; @@ -681,8 +687,13 @@ impl Config { if cloud.base_url.is_none() { cloud.base_url = Some(OLLAMA_CLOUD_BASE_URL.to_string()); } - if cloud.api_key_env.is_none() { - cloud.api_key_env = Some(OLLAMA_CLOUD_API_KEY_ENV.to_string()); + if cloud.api_key_env.is_none() + || cloud + .api_key_env + .as_deref() + .is_some_and(|value| value == LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV) + { + cloud.api_key_env = Some(OWLEN_OLLAMA_CLOUD_API_KEY_ENV.to_string()); } } } @@ -830,7 +841,7 @@ fn default_ollama_cloud_config() -> ProviderConfig { provider_type: canonical_provider_type("ollama_cloud"), base_url: Some(OLLAMA_CLOUD_BASE_URL.to_string()), api_key: None, - api_key_env: Some(OLLAMA_CLOUD_API_KEY_ENV.to_string()), + api_key_env: Some(OWLEN_OLLAMA_CLOUD_API_KEY_ENV.to_string()), extra, } } @@ -918,7 +929,10 @@ fn is_cloud_base_url(base_url: Option<&String>) -> bool { base_url .map(|url| { let trimmed = url.trim_end_matches('/'); - trimmed == OLLAMA_CLOUD_BASE_URL || trimmed.starts_with("https://ollama.com/") + trimmed == OLLAMA_CLOUD_BASE_URL + || trimmed == LEGACY_OLLAMA_CLOUD_BASE_URL + || trimmed.starts_with("https://ollama.com/") + || trimmed.starts_with("https://api.ollama.com/") }) .unwrap_or(false) } diff --git a/crates/owlen-core/src/providers/ollama.rs b/crates/owlen-core/src/providers/ollama.rs index e4cef1e..34f8e56 100644 --- a/crates/owlen-core/src/providers/ollama.rs +++ b/crates/owlen-core/src/providers/ollama.rs @@ -36,7 +36,10 @@ use uuid::Uuid; use crate::{ Error, Result, - config::{GeneralSettings, OLLAMA_CLOUD_BASE_URL, OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY}, + config::{ + GeneralSettings, LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL, + OLLAMA_CLOUD_ENDPOINT_KEY, OLLAMA_MODE_KEY, OWLEN_OLLAMA_CLOUD_API_KEY_ENV, + }, llm::{LlmProvider, ProviderConfig}, mcp::McpToolDescriptor, model::{DetailedModelInfo, ModelDetailsCache, ModelManager}, @@ -387,6 +390,8 @@ impl OllamaProvider { let mut api_key = resolve_api_key(config.api_key.clone()) .or_else(|| resolve_api_key_env_hint(config.api_key_env.as_deref())) + .or_else(|| env_var_non_empty(OWLEN_OLLAMA_CLOUD_API_KEY_ENV)) + .or_else(|| env_var_non_empty(LEGACY_OLLEN_OLLAMA_CLOUD_API_KEY_ENV)) .or_else(|| env_var_non_empty("OLLAMA_API_KEY")) .or_else(|| env_var_non_empty("OLLAMA_CLOUD_API_KEY")); let api_key_present = api_key.is_some();