Skip to content

feat: add OpenCode as an AI editing provider#90

Open
n00ki wants to merge 4 commits intoerictli:mainfrom
n00ki:feat/opencode-cli-integration
Open

feat: add OpenCode as an AI editing provider#90
n00ki wants to merge 4 commits intoerictli:mainfrom
n00ki:feat/opencode-cli-integration

Conversation

@n00ki
Copy link

@n00ki n00ki commented Mar 5, 2026

  • Add OpenCode as an optional AI editing provider alongside Claude Code, OpenAI Codex, and Ollama.
  • Wire OpenCode through the full app flow: command palette, AI modal CLI checks, execution path, loading overlay, and response toast.
  • Update docs and UX polish (official OpenCode icon geometry + gentle pulse loading animation).

What Changed

Backend

  • Added new commands:
    • ai_check_opencode_cli
    • ai_execute_opencode
  • Registered both commands in the Tauri command handler.
  • Extended shared AI CLI executor to support optional:
    • working directory (current_dir)
    • per-provider environment overrides (extra_env)
  • OpenCode execution uses:
    • opencode run --file <note> -- <prompt>
    • markdown-only + canonical path + notes-folder boundary checks
    • OPENCODE_PERMISSION env override for non-interactive execution
  • Added ~/.bun/bin to expanded PATH lookup for CLI detection.

Frontend

  • Added OpenCode to AiProvider union and service wrappers:
    • checkOpenCodeCli()
    • executeOpenCodeEdit()
  • Added "Edit with OpenCode" to command palette.
  • Updated AI modal provider mappings:
    • icon, display name, CLI name, install URL, CLI check.
  • Updated app-level execution branch and loading overlay copy/icon.
  • Updated response toast provider icon handling.
  • Added OpenCode icon component using official brand geometry.
  • Added animate-pulse-gentle and applied it to OpenCode loading state.

Docs

  • Updated provider mentions to include OpenCode in:
    • README.md
    • CLAUDE.md

Summary by CodeRabbit

  • New Features
    • OpenCode added as a selectable AI editing provider across the app (modal, command palette, toasts, icons) with detection and invokeable edit flow.
  • Style
    • New gentle pulse animation and reduced-motion preference handling.
  • Documentation
    • README and docs updated to list OpenCode alongside Claude, Codex, and Ollama.

@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce8d85f6-dc96-4028-8d9b-cfb544a48271

📥 Commits

Reviewing files that changed from the base of the PR and between 944899a and 3ab4d73.

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

📝 Walkthrough

Walkthrough

Adds OpenCode as a new AI editing provider: implements Tauri commands to detect and run the OpenCode CLI, extends the generic AI CLI executor, wires new service APIs and frontend UI (icons, commands, modal/toast), and updates docs and styling.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md, README.md
Added "OpenCode" to CLI integration descriptions and README feature list; wording updates only.
Tauri backend
src-tauri/src/lib.rs
Added ai_check_opencode_cli() and ai_execute_opencode() commands; extended execute_ai_cli(...) signature with current_dir and extra_env; wired new commands into init and command lists.
Frontend service layer
src/services/ai.ts
Expanded AiProvider to include "opencode"; added checkOpenCodeCli() and executeOpenCodeEdit(filePath, prompt) which call the new Tauri commands.
App + AI UI
src/App.tsx, src/components/ai/AiEditModal.tsx, src/components/ai/AiResponseToast.tsx, src/components/command-palette/CommandPalette.tsx
Added OpenCode provider handling across AI edit flow: UI branches, status text, CLI detection wiring, and new "Edit with OpenCode" command; imports and exports updated to include OpenCodeIcon.
Icons
src/components/icons/index.tsx
Added exported OpenCodeIcon SVG component.
Styling
src/App.css
Added pulse-gentle keyframes and .animate-pulse-gentle class; added prefers-reduced-motion guards disabling certain animations.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend App
    participant AiService as AI Service
    participant Tauri as Tauri Backend
    participant CLI as OpenCode CLI

    User->>Frontend: Select "Edit with OpenCode"
    Frontend->>AiService: checkOpenCodeCli()
    AiService->>Tauri: ai_check_opencode_cli
    Tauri->>CLI: Check PATH for opencode
    CLI-->>Tauri: CLI found/not found
    Tauri-->>AiService: boolean result
    AiService-->>Frontend: CLI availability status

    alt CLI Available
        User->>Frontend: Confirm edit with prompt
        Frontend->>AiService: executeOpenCodeEdit(filePath, prompt)
        AiService->>Tauri: ai_execute_opencode
        Tauri->>Tauri: Validate file path & extension
        Tauri->>CLI: Execute OpenCode with prompt (with env/dir)
        CLI-->>Tauri: Edit complete
        Tauri->>Frontend: Emit UI event / update index
        AiService-->>Frontend: Execution result
        Frontend->>User: Display success/result
    else CLI Not Available
        Frontend->>User: Display installation guidance
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • erictli

Poem

🐰 A nibble of code, a hop and a cheer,
OpenCode joins the AI brigade here,
Icons gleam bright and commands softly hum,
Notes get a polish — tap, edit, and thrum,
Hooray for meadow fixes — hop, run, become!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add OpenCode as an AI editing provider' accurately and concisely summarizes the main change: adding OpenCode support as a new AI editing provider throughout the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/App.css (1)

267-281: Add reduced-motion fallback for the new pulse animation.

This animation is smooth, but it should respect prefers-reduced-motion for accessibility.

♿ Suggested CSS tweak
 .animate-pulse-gentle {
   animation: pulse-gentle 1.3s ease-in-out infinite;
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .animate-pulse-gentle {
+    animation: none;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 267 - 281, The new keyframe animation pulse-gentle
and the utility class .animate-pulse-gentle need a prefers-reduced-motion
fallback: detect the user's reduced-motion preference and disable or simplify
the animation by overriding .animate-pulse-gentle inside a `@media`
(prefers-reduced-motion: reduce) block (set animation: none or a non-animating
equivalent and ensure final static opacity/transform values are applied) so
users who request reduced motion won't see the pulsing effect.
src-tauri/src/lib.rs (1)

2501-2524: Consider extracting shared markdown-path validation into a helper.

This validation block now exists in multiple AI execute commands; centralizing it will reduce drift in future provider additions.

♻️ Refactor sketch
+fn validate_ai_target_in_notes(file_path: &str, notes_folder: &str) -> Result<(PathBuf, PathBuf), String> {
+    let path = PathBuf::from(file_path);
+    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
+    if !ext.eq_ignore_ascii_case("md") && !ext.eq_ignore_ascii_case("markdown") {
+        return Err("AI editing is only supported for markdown files".to_string());
+    }
+    let canonical = path.canonicalize().map_err(|_| "Invalid file path".to_string())?;
+    let notes_root = PathBuf::from(notes_folder)
+        .canonicalize()
+        .map_err(|_| "Invalid notes folder".to_string())?;
+    if !canonical.starts_with(&notes_root) {
+        return Err("File must be within notes folder".to_string());
+    }
+    Ok((canonical, notes_root))
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 2501 - 2524, Extract the repeated
markdown-path validation into a helper (e.g., validate_markdown_path) that
accepts (file_path: &str, state: &State<'_, AppState>) -> Result<PathBuf,
String> and performs: check file extension case-insensitively for
"md"/"markdown", canonicalize the file path (map_err -> "Invalid file path"),
canonicalize the notes_folder from state.app_config (map_err -> "Invalid notes
folder"), and ensure the canonical file path starts_with the notes_root (Err ->
"File must be within notes folder"); then replace the inline block in
ai_execute_opencode with a call to this helper and use its returned canonical
PathBuf for subsequent work so other ai_* handlers can reuse it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/lib.rs`:
- Around line 2549-2554: The OPENCODE_PERMISSION entry is overly permissive:
change the value for "OPENCODE_PERMISSION" to a minimally permissive JSON (e.g.,
set "*" to "ask" or "deny" and remove the undocumented "doom_loop" and the
explicit "external_directory":"allow" key) so operations outside the intended
scope require user approval; update the vector element that constructs
OPENCODE_PERMISSION to use the new JSON policy string and ensure only explicit,
narrowly scoped allow patterns remain if needed.

---

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 2501-2524: Extract the repeated markdown-path validation into a
helper (e.g., validate_markdown_path) that accepts (file_path: &str, state:
&State<'_, AppState>) -> Result<PathBuf, String> and performs: check file
extension case-insensitively for "md"/"markdown", canonicalize the file path
(map_err -> "Invalid file path"), canonicalize the notes_folder from
state.app_config (map_err -> "Invalid notes folder"), and ensure the canonical
file path starts_with the notes_root (Err -> "File must be within notes
folder"); then replace the inline block in ai_execute_opencode with a call to
this helper and use its returned canonical PathBuf for subsequent work so other
ai_* handlers can reuse it.

In `@src/App.css`:
- Around line 267-281: The new keyframe animation pulse-gentle and the utility
class .animate-pulse-gentle need a prefers-reduced-motion fallback: detect the
user's reduced-motion preference and disable or simplify the animation by
overriding .animate-pulse-gentle inside a `@media` (prefers-reduced-motion:
reduce) block (set animation: none or a non-animating equivalent and ensure
final static opacity/transform values are applied) so users who request reduced
motion won't see the pulsing effect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5087f373-38bb-43d6-b87d-4b7f55cc971c

📥 Commits

Reviewing files that changed from the base of the PR and between 3331c1e and cf70582.

📒 Files selected for processing (10)
  • CLAUDE.md
  • README.md
  • src-tauri/src/lib.rs
  • src/App.css
  • src/App.tsx
  • src/components/ai/AiEditModal.tsx
  • src/components/ai/AiResponseToast.tsx
  • src/components/command-palette/CommandPalette.tsx
  • src/components/icons/index.tsx
  • src/services/ai.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/lib.rs`:
- Around line 2501-2557: The OpenCode CLI invocation in ai_execute_opencode
builds args as vec!["run","--file", <path>, "--", <prompt>] but the official
syntax is `opencode run --file <path> <prompt>`; update the execute_ai_cli call
in ai_execute_opencode to remove the stray "--" separator and pass the
run_prompt directly as the final argument in the args vector so it becomes
["run","--file", canonical_path, run_prompt], leaving all other validation,
working_directory (notes_root) and OPENCODE_PERMISSION handling unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d60e34eb-5d8d-4c4a-aad5-1c47813b8a6b

📥 Commits

Reviewing files that changed from the base of the PR and between cf70582 and 944899a.

📒 Files selected for processing (2)
  • src-tauri/src/lib.rs
  • src/App.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/App.css

Comment on lines +2501 to 2557
#[tauri::command]
async fn ai_execute_opencode(
file_path: String,
prompt: String,
state: State<'_, AppState>,
) -> Result<AiExecutionResult, String> {
let folder = {
let app_config = state.app_config.read().expect("app_config read lock");
app_config.notes_folder.clone().ok_or("Notes folder not set")?
};
let path = PathBuf::from(&file_path);
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !ext.eq_ignore_ascii_case("md") && !ext.eq_ignore_ascii_case("markdown") {
return Err("AI editing is only supported for markdown files".to_string());
}
let canonical = path
.canonicalize()
.map_err(|_| "Invalid file path".to_string())?;
let notes_root = PathBuf::from(&folder)
.canonicalize()
.map_err(|_| "Invalid notes folder".to_string())?;
if !canonical.starts_with(&notes_root) {
return Err("File must be within notes folder".to_string());
}

let run_prompt = format!(
"Edit only this markdown file: {}\n\
Apply the user's instructions below directly to that file.\n\
Do not create, delete, rename, or modify any other files.\n\
User instructions:\n\
{}",
canonical.to_string_lossy(),
prompt
);

execute_ai_cli(
"OpenCode",
"opencode".to_string(),
vec![
"run".to_string(),
"--file".to_string(),
canonical.to_string_lossy().to_string(),
"--".to_string(),
run_prompt,
],
String::new(),
"OpenCode CLI not found. Please install it from https://opencode.ai".to_string(),
Some(notes_root.to_string_lossy().to_string()),
Some(vec![
(
"OPENCODE_PERMISSION".to_string(),
"{\"*\":\"deny\",\"read\":\"allow\",\"edit\":\"allow\"}".to_string(),
),
]),
)
.await
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

OpenCode CLI run command syntax OPENCODE_PERMISSION environment variable

💡 Result:

opencode run command syntax

opencode run [message..]
  • You can pass the prompt as multiple args (they’re joined into the message).
  • Useful flags include --attach <url>, --model <provider/model>, --agent, --file/-f, --format, --continue/-c, --session/-s, --fork, --share, --port. [1]

Example:

opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"

[1]

OPENCODE_PERMISSION environment variable

OPENCODE_PERMISSION is a string containing inline JSON that overrides the permissions configuration (“inlined json permissions config”). [2]

Example (ask by default, allow bash, deny edit):

export OPENCODE_PERMISSION='{"*":"ask","bash":"allow","edit":"deny"}'
opencode run "Run git status and summarize changes"

The permission values are "allow", "ask", or "deny", and permissions are keyed by tool name (e.g., bash, edit, read, etc.). [3]

Sources: [1] [2] [3]


🏁 Script executed:

# Search for OpenCode references in the codebase
rg "opencode|OPENCODE_PERMISSION" src-tauri/src/ --type rs -B 2 -A 2

Repository: erictli/scratch

Length of output: 85


🏁 Script executed:

# Look for similar ai_execute functions to understand the pattern
rg "async fn ai_execute" src-tauri/src/lib.rs -A 30

Repository: erictli/scratch

Length of output: 4533


🏁 Script executed:

# Find execute_ai_cli implementation
rg "fn execute_ai_cli" src-tauri/src/lib.rs -A 50

Repository: erictli/scratch

Length of output: 1726


🏁 Script executed:

# Look for OpenCode tests or comments
rg -i "opencode" src-tauri/src/lib.rs -B 3 -A 3

Repository: erictli/scratch

Length of output: 1497


🏁 Script executed:

# Check if there are any integration tests or examples
find . -name "*.rs" -type f | head -20

Repository: erictli/scratch

Length of output: 149


🏁 Script executed:

# Get the full ai_execute_opencode function to see all details
rg "async fn ai_execute_opencode" src-tauri/src/lib.rs -A 60

Repository: erictli/scratch

Length of output: 2155


🏁 Script executed:

# Check if there are any tests or comments about the command syntax
rg "execute_ai_cli|run_prompt" src-tauri/src/lib.rs -B 2 -A 2

Repository: erictli/scratch

Length of output: 895


Fix OpenCode CLI command syntax—the -- separator is not documented in official OpenCode CLI specs.

The command args currently produce opencode run --file <path> -- <prompt>, but the official OpenCode CLI documentation shows the syntax as opencode run [message..] with --file as a flag, with no mention of -- as a separator between options and the prompt. The correct syntax should be opencode run --file <path> <prompt>.

The validation logic (markdown-only, canonical path within notes folder) is consistent with other AI providers. The OPENCODE_PERMISSION setting is correctly formatted as a JSON string with deny-by-default and only read and edit allowed, following the official permission schema.

Note: The prompt is passed as a CLI argument rather than stdin; for extremely long prompts, this could theoretically hit OS command-line length limits (~32KB on Linux), though this is unlikely in typical usage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 2501 - 2557, The OpenCode CLI invocation
in ai_execute_opencode builds args as vec!["run","--file", <path>, "--",
<prompt>] but the official syntax is `opencode run --file <path> <prompt>`;
update the execute_ai_cli call in ai_execute_opencode to remove the stray "--"
separator and pass the run_prompt directly as the final argument in the args
vector so it becomes ["run","--file", canonical_path, run_prompt], leaving all
other validation, working_directory (notes_root) and OPENCODE_PERMISSION
handling unchanged.

@erictli
Copy link
Owner

erictli commented Mar 6, 2026

@n00ki - thank you! You went hard on these PRs. Thanks for keeping them focused, they all seem like good changes. I'll aim to get them merged before I start working on the next release.

@n00ki
Copy link
Author

n00ki commented Mar 6, 2026

@n00ki - thank you! You went hard on these PRs. Thanks for keeping them focused, they all seem like good changes. I'll aim to get them merged before I start working on the next release.

@erictli i saw Scratch on a YT video and immediately fell in love 😍 this is exactly the kind of tool i like to use!
thank you for considering these PRs, happy to help however else i can.

@erictli
Copy link
Owner

erictli commented Mar 6, 2026

@n00ki - thank you! You went hard on these PRs. Thanks for keeping them focused, they all seem like good changes. I'll aim to get them merged before I start working on the next release.

@erictli i saw Scratch on a YT video and immediately fell in love 😍 this is exactly the kind of tool i like to use! thank you for considering these PRs, happy to help however else i can.

Woah! What was the YouTube video?

@n00ki
Copy link
Author

n00ki commented Mar 6, 2026

@n00ki - thank you! You went hard on these PRs. Thanks for keeping them focused, they all seem like good changes. I'll aim to get them merged before I start working on the next release.

@erictli i saw Scratch on a YT video and immediately fell in love 😍 this is exactly the kind of tool i like to use! thank you for considering these PRs, happy to help however else i can.

Woah! What was the YouTube video?

https://youtu.be/q35Z2q2WrPE?si=W8Sc_X_C2IBcvprW
I'm not familiar with the creator, i guess the algo got me 😂

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.

2 participants