Skip to content

fix: stop Ollama word-duplication by using its HTTP API instead of the CLI - #195

Open
tilhammer wants to merge 2 commits into
erictli:mainfrom
tilhammer:fix/ollama-line-wrap-duplication
Open

fix: stop Ollama word-duplication by using its HTTP API instead of the CLI#195
tilhammer wants to merge 2 commits into
erictli:mainfrom
tilhammer:fix/ollama-line-wrap-duplication

Conversation

@tilhammer

@tilhammer tilhammer commented Aug 2, 2026

Copy link
Copy Markdown

Fixes #143.

Root cause

ai_execute_ollama shelled out to ollama run <model>, piping the prompt via stdin and capturing stdout as plain text.

Ollama's own CLI (cmd/cmd.go) has a flag-precedence quirk: it correctly detects piped/non-interactive stdin and sets opts.WordWrap = false, but a few lines later unconditionally re-derives opts.WordWrap = !nowrap from the --nowordwrap flag's default value, silently re-enabling word-wrap rendering even when piped.

With word-wrap on, Ollama's displayResponse renderer prints a word's characters as they stream in, and if the word would cross the (assumed 80-column) line boundary, it erases the partial word with an ANSI cursor-back + erase-in-line sequence and reprints it whole on the next line. On a real terminal this looks like clean wrapping. Scratch only stripped well-formed ANSI escape codes via regex — which deletes the control bytes but not the characters already printed before them — so the "erased" partial word survives in the captured text, immediately followed by the full word: founda + foundational, exactly matching the reports in this issue.

Fix

Switch ai_execute_ollama to POST http://localhost:11434/api/generate directly (stream: false, think: true) instead of spawning a subprocess. No terminal rendering is involved at all, so there's nothing to strip or repair.

As a side benefit, Ollama returns response and thinking as separate JSON fields (confirmed against its GenerateResponse struct), so enabling thinking no longer risks it leaking into note content — only response is ever written to the file. This also let us drop the separate ollama show pre-check subprocess (a 404 from /api/generate now drives the "model not found" message directly) and the CLI-stderr string matching, in favor of real HTTP status codes and Ollama's structured {"error": "..."} body.

Testing

  • cargo check and cargo clippy -- -D warnings pass clean.
  • Manually verified against a running local Ollama instance: multi-line generation/edits no longer duplicate or truncate words at line boundaries, and reasoning models (think: true) no longer mix <think> content into the saved note.

Summary by CodeRabbit

  • New Features

    • Editing requests now connect directly to the local Ollama service.
    • Supports model selection, generated Markdown output, and clearer handling of connection, authentication, and missing-model errors.
  • Bug Fixes

    • Prevents empty generated content from overwriting files.
    • Ensures supplementary thinking output is excluded from saved edits.
    • Adds timeout handling for requests that take too long.

…e CLI

Fixes erictli#143. Scratch's ollama integration shelled out to `ollama run
<model>`, piping the prompt via stdin and capturing stdout as plain
text. Ollama's CLI re-enables its terminal word-wrap renderer by
default even when piped (a flag-precedence quirk in cmd/cmd.go: the
--nowordwrap flag default unconditionally overrides the correct
non-interactive detection). That renderer erases a partially-printed
word with ANSI cursor-back + erase-in-line, then reprints it whole on
the next line - real terminals render this as clean wrapping, but our
regex-based ANSI stripping only deleted the escape codes, not the
characters already printed before them, leaving literal duplicated
text ("founda" immediately followed by "foundational").

Switch ai_execute_ollama to POST http://localhost:11434/api/generate
directly (stream: false, think: true) instead of spawning a
subprocess. Ollama returns "response" and "thinking" as separate JSON
fields, so enabling thinking no longer risks it leaking into note
content - only "response" is ever written to the file. This also
drops the separate `ollama show` pre-check subprocess (a 404 from
/api/generate now drives the "model not found" message directly) and
the CLI-stderr string matching in favor of real HTTP status codes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e9212d6c-160f-4d40-adf8-c05bac6c4274

📥 Commits

Reviewing files that changed from the base of the PR and between 1a9763a and 6771946.

📒 Files selected for processing (1)
  • src-tauri/src/lib.rs

📝 Walkthrough

Walkthrough

The Ollama integration now uses the local /api/generate HTTP endpoint. It sends a markdown-editing prompt, handles API errors, validates generated output, and writes the result to the canonical file.

Changes

Ollama HTTP integration

Layer / File(s) Summary
HTTP generation and file write
src-tauri/Cargo.toml, src-tauri/src/lib.rs
Adds reqwest with JSON support. ai_execute_ollama selects a model, sends the editing prompt to Ollama, handles connection, timeout, HTTP, missing-model, authentication, and parsing errors, discards thinking content, validates non-empty markdown, and writes the edited file.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: erictli

Sequence Diagram(s)

sequenceDiagram
  participant ai_execute_ollama
  participant Ollama_HTTP_API
  participant Canonical_File
  ai_execute_ollama->>Ollama_HTTP_API: POST /api/generate with model and editing prompt
  Ollama_HTTP_API-->>ai_execute_ollama: Generated markdown and thinking fields
  ai_execute_ollama->>Canonical_File: Write validated markdown
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes replacing the Ollama CLI with the HTTP API to fix word duplication.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src-tauri/src/lib.rs (2)

3409-3409: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Honor OLLAMA_HOST instead of hardcoding the endpoint.

Ollama users can move the server to another host or port through the OLLAMA_HOST environment variable. This call always targets http://localhost:11434, so those setups fail with the "Could not connect" message. Read the variable and fall back to the current default.

Note that a remote or https:// host also requires a TLS feature on reqwest, which src-tauri/Cargo.toml currently disables.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` at line 3409, Update the Ollama request in the
surrounding generation flow to read the OLLAMA_HOST environment variable and
fall back to http://localhost:11434 when it is unset, then use that value for
the POST endpoint. Enable the reqwest TLS feature in src-tauri/Cargo.toml so
HTTPS Ollama hosts are supported.

3488-3499: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Strip code fences before overwriting the note.

The prompt asks the model not to wrap output in fences, but that instruction is advisory. Small local models often return ```markdown wrappers anyway. This code writes the response directly to the canonical file, so a fenced response replaces the user's note with corrupted content and the original is lost.

Add a guard that removes a leading and trailing fence before the write.

♻️ Proposed guard
-    let edited_content = body.response.trim().to_string();
+    let mut edited_content = body.response.trim().to_string();
+    // Models sometimes ignore the "no code fences" instruction.
+    if edited_content.starts_with("```") && edited_content.ends_with("```") {
+        if let Some(rest) = edited_content.split_once('\n').map(|(_, rest)| rest) {
+            edited_content = rest
+                .trim_end()
+                .trim_end_matches("```")
+                .trim_end()
+                .to_string();
+        }
+    }
     if edited_content.is_empty() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 3488 - 3499, Before the empty-output check
and write in the edited-content flow, detect a response wrapped in leading and
trailing triple-backtick fences, remove the optional language line and closing
fence, then trim the result. Keep the guard scoped to fenced responses and
continue rejecting content that becomes empty before writing through the
existing canonical-file path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/lib.rs`:
- Around line 3407-3419: Update the Ollama request in the client flow around
reqwest::Client::new and the POST to /api/generate to apply a generous request
timeout before send(). Handle timeout errors separately from other transport
failures, reporting a clear timeout-specific error while preserving the existing
handling for non-timeout failures.
- Around line 3410-3417: Remove the explicit "think": true option from the
Ollama request constructed in the visible .json payload, allowing each
configured model to use its default thinking behavior. Preserve the existing
model, prompt, and non-streaming request fields so edited Markdown is not
discarded for models that do not support thinking.
- Around line 3472-3486: The OllamaGenerateResponse parsing must accept a null
thinking field and convert JSON parse failures into the existing failed
AiExecutionResult shape. Change OllamaGenerateResponse.thinking to an optional
string, preserve the discarded-output logging only when a value is present, and
replace the response.json() error propagation with the same success: false
result used by other Ollama failure paths.

---

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Line 3409: Update the Ollama request in the surrounding generation flow to
read the OLLAMA_HOST environment variable and fall back to
http://localhost:11434 when it is unset, then use that value for the POST
endpoint. Enable the reqwest TLS feature in src-tauri/Cargo.toml so HTTPS Ollama
hosts are supported.
- Around line 3488-3499: Before the empty-output check and write in the
edited-content flow, detect a response wrapped in leading and trailing
triple-backtick fences, remove the optional language line and closing fence,
then trim the result. Keep the guard scoped to fenced responses and continue
rejecting content that becomes empty before writing through the existing
canonical-file path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1397fcc8-3153-4d11-8e60-ae3968c9c669

📥 Commits

Reviewing files that changed from the base of the PR and between 9126a5a and 1a9763a.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • src-tauri/Cargo.toml
  • src-tauri/src/lib.rs

Comment thread src-tauri/src/lib.rs Outdated
Comment thread src-tauri/src/lib.rs
Comment thread src-tauri/src/lib.rs
Addresses CodeRabbit review on erictli#195:

- reqwest::Client::new() applied no timeout, so a stalled Ollama
  request (e.g. slow model load) would hang the Tauri command forever
  with no way to recover. Build the client with a 5-minute timeout,
  matching the timeout the other AI providers already use via
  execute_ai_cli, and report timeouts with a distinct message.
- OllamaGenerateResponse.thinking was a plain String with
  #[serde(default)], which only covers a missing key - an explicit
  `"thinking": null` (which some models can send) would still fail to
  deserialize and reject the whole Tauri invoke instead of surfacing a
  normal AiExecutionResult failure. Made it Option<String> and handled
  the response-parse failure the same way every other failure in this
  function is handled (Ok(AiExecutionResult { success: false, .. })).

Not applied: the suggestion to drop `think: true` entirely. Ollama
silently no-ops the think parameter for models that don't support it
(confirmed via upstream issue reports linked in the review) and an
empty "thinking" string was already handled gracefully before this
commit - the only real gap was the null case above, which is now
fixed directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Ollama outputs duplicate/cut-off words at line ends

1 participant