From a7173ce145c2229e27f369c79ac0398a27664160 Mon Sep 17 00:00:00 2001 From: PTFOPlayer Date: Sun, 28 Jun 2026 10:54:06 +0200 Subject: [PATCH 1/6] Store provider URLs and models per-provider Replace single last_provider_url/last_model fields with per-provider maps (provider_urls, provider_models) so each provider remembers its own URL and selected model independently. Add helper methods get_url_for/set_url_for, get_model_for/set_model_for, get_current_url, get_current_model. Update all call sites and tests accordingly. Also skip the spinner animation when show_thinking is enabled, allowing the model's actual thinking content to stream in instead. --- src/agent/mod.rs | 6 +- src/agent/setup.rs | 20 ++-- src/commands/debug.rs | 4 +- src/commands/models.rs | 6 +- src/commands/settings.rs | 2 +- src/main.rs | 14 ++- tinyharness-lib/src/config/mod.rs | 157 +++++++++++++++++++++++-- tinyharness-lib/src/provider/ollama.rs | 7 ++ 8 files changed, 185 insertions(+), 31 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index b29aebb..503dc1f 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -443,8 +443,10 @@ pub async fn run_agent_loop( break; } - // Show spinner animation while waiting for first chunk - if waiting_for_first_chunk { + // Show spinner animation while waiting for first chunk. + // When show_thinking is enabled, skip the spinner — the + // model's actual thinking content will stream in instead. + if waiting_for_first_chunk && !ctx.show_thinking { let frame = SPINNER_FRAMES[spinner_idx % SPINNER_FRAMES.len()]; spinner_idx += 1; if has_shown_spinner { diff --git a/src/agent/setup.rs b/src/agent/setup.rs index cf87446..79c4a52 100644 --- a/src/agent/setup.rs +++ b/src/agent/setup.rs @@ -210,14 +210,14 @@ pub fn prompt_for_url( /// Resolve the URL to use for the given provider based on (in order of /// precedence): /// 1. `cli_url` (the value of `--url` if passed) -/// 2. `last_provider_url` from settings (only if it matches `kind`) +/// 2. URL from `settings.provider_urls` for the given `kind` /// 3. the hardcoded default for `kind` pub fn resolve_url(kind: ProviderKind, cli_url: &str, settings: &Settings) -> String { if !cli_url.is_empty() { return cli_url.to_string(); } - if let Some(saved) = &settings.last_provider_url { - return saved.clone(); + if let Some(saved) = settings.get_url_for(kind) { + return saved.to_string(); } default_url_for(kind).to_string() } @@ -240,7 +240,7 @@ pub fn save_provider_settings(kind: ProviderKind, url: &str) { // Load existing settings to preserve unrelated fields (mode, model, etc.) let mut s = tinyharness_lib::config::load_settings(); s.last_provider = kind; - s.last_provider_url = Some(url.to_string()); + s.set_url_for(kind, url.to_string()); save_settings(&s); } @@ -495,10 +495,8 @@ mod tests { #[test] fn resolve_url_cli_overrides_everything() { - let s = Settings { - last_provider_url: Some("http://saved:1234".to_string()), - ..Settings::default() - }; + let mut s = Settings::default(); + s.set_url_for(ProviderKind::Ollama, "http://saved:1234".to_string()); assert_eq!( resolve_url(ProviderKind::Ollama, "http://cli:9999", &s), "http://cli:9999" @@ -507,10 +505,8 @@ mod tests { #[test] fn resolve_url_uses_saved_when_no_cli() { - let s = Settings { - last_provider_url: Some("http://saved:1234".to_string()), - ..Settings::default() - }; + let mut s = Settings::default(); + s.set_url_for(ProviderKind::Ollama, "http://saved:1234".to_string()); assert_eq!( resolve_url(ProviderKind::Ollama, "", &s), "http://saved:1234" diff --git a/src/commands/debug.rs b/src/commands/debug.rs index 2f30a81..dab726d 100644 --- a/src/commands/debug.rs +++ b/src/commands/debug.rs @@ -287,7 +287,7 @@ fn dump_provider_diagnostics(file: &mut std::fs::File, _ctx: &CommandContext) { writeln!(file, "Provider kind: {}", settings.last_provider).unwrap(); - if let Some(url) = &settings.last_provider_url { + if let Some(url) = settings.get_current_url() { writeln!(file, "Provider URL: {}", url).unwrap(); } else { writeln!(file, "Provider URL: (not set)").unwrap(); @@ -296,7 +296,7 @@ fn dump_provider_diagnostics(file: &mut std::fs::File, _ctx: &CommandContext) { writeln!( file, "Current model: {}", - settings.last_model.as_deref().unwrap_or("(none)") + settings.get_current_model().unwrap_or("(none)") ) .unwrap(); diff --git a/src/commands/models.rs b/src/commands/models.rs index 0fcedf1..19e1ccc 100644 --- a/src/commands/models.rs +++ b/src/commands/models.rs @@ -33,8 +33,10 @@ async_command!( execute_select(&mut ctx.output, &mut *p, &name).await?; let mut settings = load_settings(); - settings.last_model = p.current_model(); - save_settings(&settings); + if let Some(model) = p.current_model() { + settings.set_model_for(settings.last_provider, model); + save_settings(&settings); + } Ok(CommandResult::Ok) } diff --git a/src/commands/settings.rs b/src/commands/settings.rs index 3f54bd9..b814b65 100644 --- a/src/commands/settings.rs +++ b/src/commands/settings.rs @@ -31,7 +31,7 @@ fn execute_summary(out: &mut Output, settings: &tinyharness_lib::config::Setting let provider_str = format!("{}", settings.last_provider); let _ = writeln!(out, "{BOLD}│{RESET} Provider: {BLUE}{provider_str}{RESET}",); - match &settings.last_model { + match settings.get_current_model() { Some(model) => { let _ = writeln!(out, "{BOLD}│{RESET} Model: {BLUE}{model}{RESET}"); } diff --git a/src/main.rs b/src/main.rs index 00d28bb..2309b1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -531,7 +531,14 @@ async fn main() -> Result<(), Box> { ); } } else { - auto_select_model(&mut *p, settings.last_model.as_ref()).await; + auto_select_model( + &mut *p, + settings + .get_model_for(provider_kind) + .map(|s| s.to_string()) + .as_ref(), + ) + .await; } } @@ -549,11 +556,11 @@ async fn main() -> Result<(), Box> { if settings.last_provider != provider_kind { settings.last_provider = provider_kind; } - settings.last_provider_url = Some(url.clone()); + settings.set_url_for(provider_kind, url.clone()); save_settings(&settings); } else if !explicit_provider && !args.url.is_empty() { // User gave --url only (provider resolved from saved settings). - settings.last_provider_url = Some(url.clone()); + settings.set_url_for(provider_kind, url.clone()); save_settings(&settings); } // else: no CLI override — leave settings as they are. @@ -634,6 +641,7 @@ async fn main() -> Result<(), Box> { let mut ctx = CommandContext::new(Arc::clone(&provider), workspace_ctx, prompts_dir); ctx.current_mode = initial_mode; + ctx.show_thinking = settings.show_thinking; ctx.session_id = Some(session.id().to_string()); // Build the command registry to extract command names and subcommand diff --git a/tinyharness-lib/src/config/mod.rs b/tinyharness-lib/src/config/mod.rs index a2cf364..49b4556 100644 --- a/tinyharness-lib/src/config/mod.rs +++ b/tinyharness-lib/src/config/mod.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::PathBuf; use std::{fmt, str::FromStr}; @@ -323,7 +324,7 @@ pub fn generate_project_config_template(settings: &Settings) -> ProjectSettings } /// Identifies which provider backend was used last. -#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] pub enum ProviderKind { #[default] Ollama, @@ -420,12 +421,18 @@ impl FromStr for OllamaThinkType { pub struct Settings { #[serde(default)] pub last_provider: ProviderKind, - /// Last URL used for the active provider. Set automatically by `--config` - /// or by passing `--url`. Persisted so subsequent runs don't re-prompt. - /// (default: None) - pub last_provider_url: Option, - #[serde(default)] - pub last_model: Option, + /// Per-provider URL overrides. Keyed by `ProviderKind` so switching + /// providers remembers each one's last URL. Migrated from the legacy + /// `last_provider_url` field on load. + /// (default: empty) + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub provider_urls: HashMap, + /// Per-provider model selections. Keyed by `ProviderKind` so switching + /// providers remembers each one's last model. Migrated from the legacy + /// `last_model` field on load. + /// (default: empty) + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub provider_models: HashMap, #[serde(default)] pub preferred_mode: AgentMode, #[serde(default)] @@ -487,8 +494,8 @@ impl Default for Settings { fn default() -> Self { Settings { last_provider: ProviderKind::Ollama, - last_provider_url: None, - last_model: None, + provider_urls: HashMap::new(), + provider_models: HashMap::new(), preferred_mode: AgentMode::Casual, ollama_api_key: None, openai_compat_api_key: None, @@ -575,6 +582,36 @@ impl Settings { pub fn get_denied_commands(&self) -> Vec { self.denied_command_prefixes.clone().unwrap_or_default() } + + /// Get the saved URL for a specific provider kind, if any. + pub fn get_url_for(&self, kind: ProviderKind) -> Option<&str> { + self.provider_urls.get(&kind).map(|s| s.as_str()) + } + + /// Set the URL for a specific provider kind. + pub fn set_url_for(&mut self, kind: ProviderKind, url: String) { + self.provider_urls.insert(kind, url); + } + + /// Get the saved model for a specific provider kind, if any. + pub fn get_model_for(&self, kind: ProviderKind) -> Option<&str> { + self.provider_models.get(&kind).map(|s| s.as_str()) + } + + /// Set the model for a specific provider kind. + pub fn set_model_for(&mut self, kind: ProviderKind, model: String) { + self.provider_models.insert(kind, model); + } + + /// Get the saved model for the current (`last_provider`) kind, if any. + pub fn get_current_model(&self) -> Option<&str> { + self.get_model_for(self.last_provider) + } + + /// Get the saved URL for the current (`last_provider`) kind, if any. + pub fn get_current_url(&self) -> Option<&str> { + self.get_url_for(self.last_provider) + } } // ── Errors ────────────────────────────────────────────────────────────────── @@ -676,6 +713,32 @@ impl SettingsStore { } } + // Migrate legacy `last_model` and `last_provider_url` fields into + // the per-provider HashMaps. The key is `last_provider` (the + // provider that was active when the old config was saved). + if let Ok(raw) = serde_json::from_str::(&content) + && let Some(obj) = raw.as_object() + { + if settings.provider_models.is_empty() + && let Some(model) = obj.get("last_model") + && let Some(model_str) = model.as_str() + && !model_str.is_empty() + { + settings + .provider_models + .insert(settings.last_provider, model_str.to_string()); + } + if settings.provider_urls.is_empty() + && let Some(url) = obj.get("last_provider_url") + && let Some(url_str) = url.as_str() + && !url_str.is_empty() + { + settings + .provider_urls + .insert(settings.last_provider, url_str.to_string()); + } + } + Ok(settings) } @@ -949,6 +1012,82 @@ mod tests { assert_eq!(settings.auto_accept_mode, AutoAcceptMode::Off); } + #[test] + fn migrate_legacy_last_model_and_url() { + let store = SettingsStore::new(temp_settings_path()); + let json = r#"{"last_provider": "Ollama", "last_model": "qwen2.5:7b", "last_provider_url": "http://localhost:11434"}"#; + std::fs::write(store.path(), json).unwrap(); + let settings = store.load().unwrap(); + assert_eq!( + settings.get_model_for(ProviderKind::Ollama), + Some("qwen2.5:7b") + ); + assert_eq!( + settings.get_url_for(ProviderKind::Ollama), + Some("http://localhost:11434") + ); + let _ = std::fs::remove_file(store.path()); + } + + #[test] + fn migrate_legacy_fields_for_openai_compat() { + let store = SettingsStore::new(temp_settings_path()); + let json = r#"{"last_provider": "OpenAiCompat", "last_model": "gpt-4o", "last_provider_url": "https://api.openai.com/v1"}"#; + std::fs::write(store.path(), json).unwrap(); + let settings = store.load().unwrap(); + assert_eq!( + settings.get_model_for(ProviderKind::OpenAiCompat), + Some("gpt-4o") + ); + assert_eq!( + settings.get_url_for(ProviderKind::OpenAiCompat), + Some("https://api.openai.com/v1") + ); + // Ollama should NOT have a model from this migration + assert_eq!(settings.get_model_for(ProviderKind::Ollama), None); + let _ = std::fs::remove_file(store.path()); + } + + #[test] + fn per_provider_model_and_url_accessors() { + let mut settings = Settings::default(); + settings.set_model_for(ProviderKind::Ollama, "qwen2.5:7b".to_string()); + settings.set_model_for(ProviderKind::LlamaCpp, "llama-3.1-8b".to_string()); + settings.set_url_for(ProviderKind::LlamaCpp, "http://localhost:8080".to_string()); + + assert_eq!( + settings.get_model_for(ProviderKind::Ollama), + Some("qwen2.5:7b") + ); + assert_eq!( + settings.get_model_for(ProviderKind::LlamaCpp), + Some("llama-3.1-8b") + ); + assert_eq!(settings.get_model_for(ProviderKind::Vllm), None); + assert_eq!( + settings.get_url_for(ProviderKind::LlamaCpp), + Some("http://localhost:8080") + ); + assert_eq!(settings.get_url_for(ProviderKind::Ollama), None); + } + + #[test] + fn get_current_model_and_url_use_last_provider() { + let mut settings = Settings::default(); + settings.last_provider = ProviderKind::OpenAiCompat; + settings.set_model_for(ProviderKind::OpenAiCompat, "gpt-4o".to_string()); + settings.set_url_for( + ProviderKind::OpenAiCompat, + "https://api.openai.com/v1".to_string(), + ); + + assert_eq!(settings.get_current_model(), Some("gpt-4o")); + assert_eq!( + settings.get_current_url(), + Some("https://api.openai.com/v1") + ); + } + fn temp_settings_path() -> std::path::PathBuf { let mut dir = std::env::temp_dir(); dir.push(format!( diff --git a/tinyharness-lib/src/provider/ollama.rs b/tinyharness-lib/src/provider/ollama.rs index 0b198c4..6835d10 100644 --- a/tinyharness-lib/src/provider/ollama.rs +++ b/tinyharness-lib/src/provider/ollama.rs @@ -432,6 +432,13 @@ async fn stream_ollama_chat( let url = format!("{base_url}api/chat"); let mut request = serde_json::to_value(request).map_err(|e| format!("serialize: {e}"))?; + // Force streaming — ollama-rs sets `stream: true` only inside its own + // `send_chat_messages_stream()` method, which we bypass. Without this, + // Ollama returns the entire response as a single JSON object instead of + // streaming line-by-line, causing the UI to block until the full message + // arrives. + request["stream"] = serde_json::Value::Bool(true); + // Fix ollama-rs 0.3.4 serialization quirks for Ollama Cloud / Gemini compatibility: // // 1. ToolType::Function serializes as "Function" (uppercase F), but the From adc9c69fa3fac14f7324e332c7fa44c592ee0cfb Mon Sep 17 00:00:00 2001 From: PTFOPlayer Date: Sun, 28 Jun 2026 11:03:53 +0200 Subject: [PATCH 2/6] ci: expand workflow with matrix, docs, msrv, audit, deny, typos - Consolidate Linux/Windows build+test into a 3-OS matrix (adds macOS) - Add concurrency cancellation for superseded runs - Add docs job with RUSTDOCFLAGS=-D warnings - Add MSRV check (auto-skips when rust-version is unset) - Add cargo audit, cargo-deny, and typos jobs - Enforce --locked and -D warnings across jobs - Add timeout-minutes to every job - Clippy now scans --all-targets --- .github/workflows/ci.yml | 91 +++++++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a94b3ce..bc07f5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,17 @@ on: env: CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: fmt: name: Format runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -24,46 +30,93 @@ jobs: clippy: name: Clippy runs-on: ubuntu-latest + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --workspace -- -D warnings + - run: cargo clippy --workspace --all-targets --locked -- -D warnings - test: - name: Test + docs: + name: Docs runs-on: ubuntu-latest + timeout-minutes: 20 + env: + RUSTDOCFLAGS: -D warnings steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: cargo test --workspace + - run: cargo doc --workspace --no-deps --locked - build: - name: Build - runs-on: ubuntu-latest + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: cargo build --workspace + - run: cargo build --workspace --locked + - run: cargo test --workspace --locked - windows-build: - name: Build (Windows) - runs-on: windows-latest + msrv: + name: MSRV check + runs-on: ubuntu-latest + timeout-minutes: 20 steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - name: Read MSRV from Cargo.toml + id: msrv + shell: bash + run: | + msrv=$(grep -E '^rust-version' Cargo.toml | head -n1 | sed -E 's/.*"([^"]+)".*/\1/') + if [ -z "$msrv" ]; then + echo "No rust-version set in Cargo.toml; skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "version=$msrv" >> "$GITHUB_OUTPUT" + fi + - uses: dtolnay/rust-toolchain@master + if: steps.msrv.outputs.skip != 'true' + with: + toolchain: ${{ steps.msrv.outputs.version }} - uses: Swatinem/rust-cache@v2 - - run: cargo build --workspace + if: steps.msrv.outputs.skip != 'true' + - if: steps.msrv.outputs.skip != 'true' + run: cargo check --workspace --all-targets --locked - windows-test: - name: Test (Windows) - runs-on: windows-latest + audit: + name: Security audit + runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: cargo test --workspace + - uses: rustsec/audit-check@v2.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + deny: + name: cargo-deny + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check + arguments: --all-features + + typos: + name: Typos + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master From 55c6cfff142cb384fdeca1bbd0eb5e1e8cfc0ae2 Mon Sep 17 00:00:00 2001 From: PTFOPlayer Date: Sun, 28 Jun 2026 11:14:01 +0200 Subject: [PATCH 3/6] ci: add deny.toml, typos config, and fix MSRV grep - deny.toml: MIT-compatible permissive license allowlist; wildcards and unknown registries/git sources denied by default - .typos.toml: allowlist short test-fixture strings and accepted spellings (ratatui, unparseable, Invokable) - ci.yml: wrap MSRV grep in set +e so absent rust-version no longer fails the job --- .github/workflows/ci.yml | 2 ++ .typos.toml | 25 +++++++++++++++++++++++ deny.toml | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 .typos.toml create mode 100644 deny.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc07f5f..270478a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,9 @@ jobs: id: msrv shell: bash run: | + set +e msrv=$(grep -E '^rust-version' Cargo.toml | head -n1 | sed -E 's/.*"([^"]+)".*/\1/') + set -e if [ -z "$msrv" ]; then echo "No rust-version set in Cargo.toml; skipping." echo "skip=true" >> "$GITHUB_OUTPUT" diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..6152d5b --- /dev/null +++ b/.typos.toml @@ -0,0 +1,25 @@ +# typos configuration: https://github.com/crate-ci/typos +# Allowlist false positives from test fixtures, technical names, and common +# short tokens that the dictionary flags too aggressively. + +[default] +extend-ignore-re = [ + # Ignore short truncated-string test fixtures, e.g. truncate("hello", 3) == "hel" + '"hel"', + '"Hello Worl"', + 'b"fo"', +] + +[default.extend-words] +# Project / crate names +ratatui = "ratatui" +# Accepted alternate spellings used in this codebase +unparseable = "unparseable" +Invokable = "Invokable" + +[files] +extend-exclude = [ + "target/", + "*.lock", + "tests/sockudo/", +] diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..d0a9b16 --- /dev/null +++ b/deny.toml @@ -0,0 +1,43 @@ +# cargo-deny configuration for TinyHarness (MIT-licensed). +# Docs: https://embarkstudios.github.io/cargo-deny/ + +[advisories] +# Fail on any RUSTSEC advisory. Add IDs here with a reason if intentionally ignored. +ignore = [] + +[licenses] +# Permissive licenses compatible with redistribution under MIT. +# Copyleft licenses (GPL/LGPL/AGPL/MPL/EPL/CDDL/SSPL/BUSL) are intentionally +# omitted — add only after legal review. +allow = [ + "MIT", + "MIT-0", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-2-Clause-Patent", + "BSD-3-Clause", + "ISC", + "0BSD", + "Zlib", + "CC0-1.0", + "Unlicense", + "BSL-1.0", + "CDLA-Permissive-2.0", # Mozilla CA bundle (webpki-root-certs) + "Unicode-3.0", # unicode-ident, icu + "Unicode-DFS-2016", + "OpenSSL", +] +confidence-threshold = 0.8 + +[bans] +multiple-versions = "warn" +wildcards = "deny" +# Workspace members aren't published; allow path-only wildcards if any appear. +allow-wildcard-paths = true + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] From 720eec55d4622f68e0d63931e28ab0f203bc70b9 Mon Sep 17 00:00:00 2001 From: PTFOPlayer Date: Sun, 28 Jun 2026 11:15:44 +0200 Subject: [PATCH 4/6] ci: bump actions/checkout v4 -> v5 (Node 20 deprecation) --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 270478a..4200d23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: components: clippy @@ -46,7 +46,7 @@ jobs: env: RUSTDOCFLAGS: -D warnings steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo doc --workspace --no-deps --locked @@ -60,7 +60,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo build --workspace --locked @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Read MSRV from Cargo.toml id: msrv shell: bash @@ -99,7 +99,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: rustsec/audit-check@v2.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -109,7 +109,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check @@ -120,5 +120,5 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: crate-ci/typos@master From 0ec70c4263e5ee6c7e4134a4e18d1f28037101a1 Mon Sep 17 00:00:00 2001 From: PTFOPlayer Date: Sun, 28 Jun 2026 11:24:25 +0200 Subject: [PATCH 5/6] fix: resolve CI clippy and rustdoc errors clippy (tests with -D warnings): - config::tests: use struct-literal init instead of field reassign on default - screen.rs: prefix unused end_row test bindings with _ - conversation.rs: prefix unused row loop variable with _ - sockudo_integration: allow(too_many_arguments) on test helper rustdoc (with -D warnings): - config::mod: split intra-doc links to valid items (no chained method links) - mode.rs: wrap placeholder in backticks to avoid HTML parsing - screen.rs: drop links to private write_wrapped, use backtick code instead - image.rs, registry.rs: wrap // placeholders in backticks - main.rs: wrap Arc in backticks - registry.rs: convert broken intra-doc link freeze_descriptions to code --- src/commands/image.rs | 8 ++++---- src/commands/registry.rs | 4 ++-- src/main.rs | 2 +- tinyharness-lib/src/config/mod.rs | 12 ++++++++---- tinyharness-lib/src/mode.rs | 2 +- tinyharness-lib/tests/sockudo_integration.rs | 1 + tinyharness-ui/src/tui/screen.rs | 10 +++++----- tinyharness-ui/src/tui/widgets/conversation.rs | 2 +- 8 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/commands/image.rs b/src/commands/image.rs index e13f50e..4ca677a 100644 --- a/src/commands/image.rs +++ b/src/commands/image.rs @@ -11,10 +11,10 @@ const MAX_PENDING_IMAGES: usize = 10; /// Execute the `/image` command. /// /// Usage: -/// /image — Attach an image file -/// /image — Show pending images -/// /image clear — Clear all pending images -/// /image drop — Remove a specific pending image by index +/// `/image ` — Attach an image file +/// `/image` — Show pending images +/// `/image clear` — Clear all pending images +/// `/image drop ` — Remove a specific pending image by index pub fn execute(ctx: &mut CommandContext, arg: Option<&str>) { match arg { None | Some("") => { diff --git a/src/commands/registry.rs b/src/commands/registry.rs index 0f47163..05f5a33 100644 --- a/src/commands/registry.rs +++ b/src/commands/registry.rs @@ -165,7 +165,7 @@ pub trait Command: Send + Sync { /// One-line description for /help. fn description(&self) -> &'static str; - /// Usage string (e.g., "/model "). Defaults to the command name. + /// Usage string (e.g., `/model `). Defaults to the command name. fn usage(&self) -> &'static str { self.name() } @@ -310,7 +310,7 @@ pub struct CommandRegistry { /// Alias descriptions for /help display. alias_descriptions: HashMap<&'static str, &'static str>, /// Pre-computed (usage, description) pairs for /help, including aliases. - /// Populated by [`freeze_descriptions`] after all registrations are done. + /// Populated by `freeze_descriptions` after all registrations are done. descriptions: Vec<(&'static str, &'static str)>, /// Subcommand completions for tab-completion (command name → argument completions). subcommands: HashMap>, diff --git a/src/main.rs b/src/main.rs index 2309b1e..209aa6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,7 +116,7 @@ fn resolve_provider_kind(args: &Args, settings: &Settings) -> ProviderKind { } } -/// Create the provider backend, run health checks, and return it wrapped in Arc. +/// Create the provider backend, run health checks, and return it wrapped in `Arc`. #[allow(clippy::too_many_arguments)] async fn create_provider( kind: ProviderKind, diff --git a/tinyharness-lib/src/config/mod.rs b/tinyharness-lib/src/config/mod.rs index 49b4556..ae3aba3 100644 --- a/tinyharness-lib/src/config/mod.rs +++ b/tinyharness-lib/src/config/mod.rs @@ -786,14 +786,16 @@ impl SettingsStore { /// Load settings from the default path, returning defaults on any error. /// -/// This is a convenience wrapper around [`SettingsStore::default_path().load_or_default()`]. +/// This is a convenience wrapper around +/// [`SettingsStore::default_path`] followed by [`SettingsStore::load_or_default`]. pub fn load_settings() -> Settings { SettingsStore::default_path().load_or_default() } /// Save settings to the default path atomically. /// -/// This is a convenience wrapper around [`SettingsStore::default_path().save()`]. +/// This is a convenience wrapper around +/// [`SettingsStore::default_path`] followed by [`SettingsStore::save`]. /// On error, prints a warning to stderr (matching the original behaviour). pub fn save_settings(settings: &Settings) { let store = SettingsStore::default_path(); @@ -1073,8 +1075,10 @@ mod tests { #[test] fn get_current_model_and_url_use_last_provider() { - let mut settings = Settings::default(); - settings.last_provider = ProviderKind::OpenAiCompat; + let mut settings = Settings { + last_provider: ProviderKind::OpenAiCompat, + ..Settings::default() + }; settings.set_model_for(ProviderKind::OpenAiCompat, "gpt-4o".to_string()); settings.set_url_for( ProviderKind::OpenAiCompat, diff --git a/tinyharness-lib/src/mode.rs b/tinyharness-lib/src/mode.rs index afd6baa..eb94a2c 100644 --- a/tinyharness-lib/src/mode.rs +++ b/tinyharness-lib/src/mode.rs @@ -50,7 +50,7 @@ impl AgentMode { /// Load the system prompt for this mode from `.md` files in `prompts_dir`. /// /// For Agent, Planning, and Research modes, the prompt is assembled as: - /// header.md + blank line + .md + /// `header.md` + blank line + `.md` /// /// Casual mode uses only its own file (self-contained). /// diff --git a/tinyharness-lib/tests/sockudo_integration.rs b/tinyharness-lib/tests/sockudo_integration.rs index db9ea54..6a8705d 100644 --- a/tinyharness-lib/tests/sockudo_integration.rs +++ b/tinyharness-lib/tests/sockudo_integration.rs @@ -410,6 +410,7 @@ fn hex_encode(bytes: &[u8]) -> String { } /// Publish a versioned message event to Sockudo via signed HTTP POST. +#[allow(clippy::too_many_arguments)] async fn publish_versioned_event( client: &reqwest::Client, app_id: &str, diff --git a/tinyharness-ui/src/tui/screen.rs b/tinyharness-ui/src/tui/screen.rs index e41dcbe..a98be89 100644 --- a/tinyharness-ui/src/tui/screen.rs +++ b/tinyharness-ui/src/tui/screen.rs @@ -202,7 +202,7 @@ impl Screen { /// If `wrap` is true, text wraps to the next line. If false, text is /// truncated at the right edge. Uses Unicode display widths. /// - /// This is a convenience wrapper around [`Self::write_wrapped`] with + /// This is a convenience wrapper around `Self::write_wrapped` with /// simple wrapping bounded by the screen dimensions. pub fn write_str_wrapped( &mut self, @@ -239,7 +239,7 @@ impl Screen { /// `max_row` is the maximum row; text stops when `row > max_row`. /// `left_margin` is the column where wrapped lines start. Uses Unicode display widths. /// - /// This is a convenience wrapper around [`Self::write_wrapped`] with + /// This is a convenience wrapper around `Self::write_wrapped` with /// `skip_rows = 0` and wrapping enabled. pub fn write_str_wrapped_clipped( &mut self, @@ -279,7 +279,7 @@ impl Screen { /// `max_row` is the maximum row; text stops when `row > max_row`. /// `left_margin` is the column where wrapped lines start. Uses Unicode display widths. /// - /// This is a convenience wrapper around [`Self::write_wrapped`] with + /// This is a convenience wrapper around `Self::write_wrapped` with /// `skip_rows > 0`. pub fn write_str_wrapped_skip_clipped( &mut self, @@ -1122,7 +1122,7 @@ mod tests { fn test_screen_write_str_wrapped_clipped_multiline() { // Test clipped wrapping with a multi-line message let mut s = Screen::new(10, 5); - let end_row = s.write_str_wrapped_clipped( + let _end_row = s.write_str_wrapped_clipped( 0, 2, "AB CD", @@ -1167,7 +1167,7 @@ mod tests { fn test_screen_write_str_wrapped_clipped_with_left_margin() { // Test wrapped clipping with left margin (like conversation messages) let mut s = Screen::new(20, 5); - let end_row = s.write_str_wrapped_clipped( + let _end_row = s.write_str_wrapped_clipped( 0, 7, "Hello World This Is A Long Message That Wraps", diff --git a/tinyharness-ui/src/tui/widgets/conversation.rs b/tinyharness-ui/src/tui/widgets/conversation.rs index 77c693f..d4468dc 100644 --- a/tinyharness-ui/src/tui/widgets/conversation.rs +++ b/tinyharness-ui/src/tui/widgets/conversation.rs @@ -2762,7 +2762,7 @@ mod visual_tests { // Mark columns beyond conversation width (simulating sidebar) let conv_width = width; - for row in 0..conv_height { + for _row in 0..conv_height { // (no sidebar in this test, but we could add one) } From 53a10a5048ae88600b7097f6feeb587e24f31c78 Mon Sep 17 00:00:00 2001 From: PTFOPlayer Date: Sun, 28 Jun 2026 11:31:48 +0200 Subject: [PATCH 6/6] deps: bump quinn-proto 0.11.14 -> 0.11.15 (RUSTSEC-2026-0185) Fixes remote memory exhaustion from unbounded out-of-order stream reassembly in quinn-proto Assembler. Pulled in transitively via reqwest -> ... -> quinn-proto. --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45649c2..7d03708 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1309,9 +1309,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes",