feat: add OpenCode as an AI editing provider#90
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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-motionfor 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(¬es_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
📒 Files selected for processing (10)
CLAUDE.mdREADME.mdsrc-tauri/src/lib.rssrc/App.csssrc/App.tsxsrc/components/ai/AiEditModal.tsxsrc/components/ai/AiResponseToast.tsxsrc/components/command-palette/CommandPalette.tsxsrc/components/icons/index.tsxsrc/services/ai.ts
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src-tauri/src/lib.rssrc/App.css
🚧 Files skipped from review as they are similar to previous changes (1)
- src/App.css
| #[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(¬es_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 | ||
| } |
There was a problem hiding this comment.
🧩 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 2Repository: 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 30Repository: erictli/scratch
Length of output: 4533
🏁 Script executed:
# Find execute_ai_cli implementation
rg "fn execute_ai_cli" src-tauri/src/lib.rs -A 50Repository: 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 3Repository: erictli/scratch
Length of output: 1497
🏁 Script executed:
# Check if there are any integration tests or examples
find . -name "*.rs" -type f | head -20Repository: 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 60Repository: 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 2Repository: 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.
|
@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! |
Woah! What was the YouTube video? |
https://youtu.be/q35Z2q2WrPE?si=W8Sc_X_C2IBcvprW |
What Changed
Backend
ai_check_opencode_cliai_execute_opencodecurrent_dir)extra_env)opencode run --file <note> -- <prompt>OPENCODE_PERMISSIONenv override for non-interactive execution~/.bun/binto expanded PATH lookup for CLI detection.Frontend
AiProviderunion and service wrappers:checkOpenCodeCli()executeOpenCodeEdit()animate-pulse-gentleand applied it to OpenCode loading state.Docs
README.mdCLAUDE.mdSummary by CodeRabbit