Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c486047
Update opencode-review.yml
Jordonbc Apr 23, 2026
169aad6
Fix issues
Jordonbc Apr 26, 2026
91d0e37
Fix issues
Jordonbc Apr 26, 2026
8340c7f
Fix issues
Jordonbc Apr 26, 2026
ee7865e
Fixed commit selection issues
Jordonbc Apr 26, 2026
d8e4334
Update PluginList.png
Jordonbc Apr 26, 2026
bcc3b87
Update ShowHistory.png
Jordonbc Apr 26, 2026
27e5241
backend: remove ssh env mutation
Jordonbc Apr 26, 2026
3cf96ad
frontend: fix commit selection flow
Jordonbc Apr 26, 2026
053e7fc
frontend: sanitize stash context paths
Jordonbc Apr 26, 2026
50883ac
3 bugs fixed + 2 additional fixes applied
opencode-agent[bot] Apr 26, 2026
ae6522b
Revert "3 bugs fixed + 2 additional fixes applied"
Jordonbc Apr 26, 2026
d4dde42
Update AGENTS.md
Jordonbc Apr 26, 2026
5ee2b53
PR approved, fix applied, tests pass
opencode-agent[bot] Apr 26, 2026
5ffedb2
Update opencode-review.yml
Jordonbc Apr 26, 2026
9faffa6
Merge branch 'Fix-bugs' of github.com:Open-VCS/OpenVCS into Fix-bugs
Jordonbc Apr 26, 2026
58ecb98
Revert "Merge branch 'Fix-bugs' of github.com:Open-VCS/OpenVCS into F…
Jordonbc Apr 26, 2026
b5844cc
PR review: 3 bugs fixed, tests pass
opencode-agent[bot] Apr 26, 2026
d3588e3
Revert "PR review: 3 bugs fixed, tests pass"
Jordonbc Apr 26, 2026
f65dc50
Update opencode-review.yml
Jordonbc Apr 26, 2026
4678cc1
Update opencode-review.yml
Jordonbc Apr 26, 2026
0e4a79f
Improve tauri checks
Jordonbc Apr 26, 2026
52d5024
Improved frontend
Jordonbc Apr 26, 2026
492802d
Potential fix for pull request finding 'Unused variable, import, func…
Jordonbc Apr 26, 2026
743cda2
Merge pull request #227 from Open-VCS/Fix-bugs
Jordonbc Apr 26, 2026
cb7fd83
Fix issues
Jordonbc Apr 26, 2026
4ca53a1
Merge pull request #228 from Open-VCS/Fix-Issues
Jordonbc Apr 26, 2026
c1af415
Update stash.ts
Jordonbc Apr 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
contents: write
pull-requests: write
issues: write

Expand All @@ -26,28 +26,32 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.ZEN_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
#GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
model: ${{ vars.OPENCODE_REVIEW_MODEL }}
use_github_token: false
use_github_token: true
prompt: |
Before anything else, read and follow AGENTS.md for this repository and module.
Review this pull request:
- Check for code quality issues
- Look for potential bugs
- Suggest improvements
- If you make any code edits, run 'cd Client && just test' to verify they compile and pass tests before committing.

- name: fallback
if: ${{ steps.review_primary.outcome == 'failure' }}
uses: anomalyco/opencode/github@2410593023d2c61f05123c9b0faf189a28dfbeee
env:
OPENCODE_API_KEY: ${{ secrets.ZEN_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
#GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
model: ${{ vars.OPENCODE_REVIEW_MODEL_FALLBACK }}
use_github_token: false
use_github_token: true
prompt: |
Before anything else, read and follow AGENTS.md for this repository and module.
Review this pull request:
- Check for code quality issues
- Look for potential bugs
- Suggest improvements
- If you make any code edits, run 'cd Client && just test' to verify they compile and pass tests before committing.
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- `Backend/`: Rust + Tauri backend (`src/`), commands (`src/tauri_commands/`), plugin runtime (`src/plugin_runtime/`), and config-driven plugin sync support (`scripts/`).
- `openvcs.plugins.json`: built-in plugin source list used to materialize shipped plugins during client builds.
- `Frontend/`: TypeScript + Vite UI code (`src/scripts/`, `src/styles/`, `src/modals/`), with Vitest tests colocated as `*.test.ts` files.
- OpenVCS is desktop-only: there is no web app, no standalone browser mode, and no supported web browser/WebView deployment target.
- `docs/`: UX docs, plugin architecture notes, and plugin/theme packaging guides referenced by contributors.
- `packaging/flatpak/`: Flatpak manifests and Flatpak-specific build notes.
- Supporting files at the repo root include the workspace `Cargo.toml`, `Justfile`, `README.md`, `ARCHITECTURE.md`, `SECURITY.md`, and installer scripts.
Expand Down Expand Up @@ -47,7 +48,7 @@
### Development servers

- `cargo tauri dev`: run the desktop app in dev mode (`Backend/` directory).
- `npm --prefix Frontend run dev`: run the frontend-only Vite dev server.
- `npm --prefix Frontend run dev`: run the frontend-only Vite dev server for desktop UI development only; it is not a web app/browser deployment.

## Plugin runtime & host expectations

Expand Down
61 changes: 50 additions & 11 deletions Backend/src/plugin_runtime/node_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use serde::Deserialize;
use serde_json::{json, Value};
use std::io::BufReader;
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc::{channel, Receiver};
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
Expand All @@ -45,6 +45,8 @@ struct NodeRpcProcess {
rx: Receiver<Value>,
/// Flag to signal the reader thread to stop.
shutdown_flag: Arc<Mutex<bool>>,
/// Last reader-thread error observed while consuming framed messages.
reader_error: Arc<Mutex<Option<String>>>,
/// Monotonic request id counter.
next_request_id: u64,
}
Expand All @@ -62,7 +64,7 @@ impl NodeRpcProcess {
///
/// # Returns
/// - `Ok(T)` decoded response result.
/// - `Err(String)` when transport/protocol/plugin errors or timeout occur.
/// - `Err(String)` when transport/protocol/plugin errors, disconnects, or timeout occur.
fn call<T>(
&mut self,
method: &str,
Expand Down Expand Up @@ -94,14 +96,41 @@ impl NodeRpcProcess {
let start = std::time::Instant::now();
let mut remaining = timeout;
loop {
let message = self.rx.recv_timeout(remaining).map_err(|_| {
format!(
"plugin '{}' rpc '{}' timed out after {}s",
plugin_id,
method,
timeout.as_secs()
)
})?;
let message = match self.rx.recv_timeout(remaining) {
Ok(message) => message,
Err(RecvTimeoutError::Timeout) => {
return Err(format!(
"plugin '{}' rpc '{}' timed out after {}s",
plugin_id,
method,
timeout.as_secs()
));
}
Err(RecvTimeoutError::Disconnected) => {
let exit_status = self.child.try_wait().map_err(|e| {
format!("check plugin process status for '{plugin_id}': {e}")
})?;
let reader_error = self.reader_error.lock().clone();
return match (exit_status, reader_error) {
(Some(status), Some(error)) => Err(format!(
"plugin '{}' rpc '{}' disconnected because the process exited with {} (reader error: {})",
plugin_id, method, status, error
)),
(Some(status), None) => Err(format!(
"plugin '{}' rpc '{}' disconnected because the process exited with {}",
plugin_id, method, status
)),
(None, Some(error)) => Err(format!(
"plugin '{}' rpc '{}' disconnected while waiting for a response (reader error: {})",
plugin_id, method, error
)),
(None, None) => Err(format!(
"plugin '{}' rpc '{}' disconnected while waiting for a response",
plugin_id, method
)),
};
}
};

if let Some(method_name) = message.get("method").and_then(Value::as_str) {
let params = message.get("params").cloned().unwrap_or(Value::Null);
Expand Down Expand Up @@ -253,8 +282,10 @@ impl NodePluginRuntimeInstance {

let (tx, rx) = channel::<Value>();
let shutdown_flag = Arc::new(Mutex::new(false));
let reader_error = Arc::new(Mutex::new(None));
let stdout_for_thread = BufReader::new(stdout);
let shutdown_for_thread = Arc::clone(&shutdown_flag);
let reader_error_for_thread = Arc::clone(&reader_error);

thread::spawn(move || {
let mut stdout = stdout_for_thread;
Expand All @@ -269,6 +300,7 @@ impl NodePluginRuntimeInstance {
}
}
Err(e) => {
*reader_error_for_thread.lock() = Some(e.to_string());
debug!("node rpc reader thread: read error: {}", e);
break;
}
Expand All @@ -281,6 +313,7 @@ impl NodePluginRuntimeInstance {
stdin,
rx,
shutdown_flag,
reader_error,
next_request_id: 1,
};

Expand Down Expand Up @@ -336,7 +369,13 @@ impl NodePluginRuntimeInstance {
let process = lock
.as_mut()
.ok_or_else(|| "node runtime did not initialize".to_string())?;
f(process)
let result = f(process);
if let Err(err) = &result {
if err.contains("disconnected") {
lock.take();
}
}
result
}

/// Sends one RPC request to the plugin process.
Expand Down
28 changes: 23 additions & 5 deletions Backend/src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,21 @@ impl PluginCache {

fn list(&self) -> Vec<PluginSummary> {
self.ensure_fresh();
self.data.read().unwrap().list.clone()
self.data
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.list
.clone()
}

fn load_cached_plugin(&self, id: &str) -> Option<CachedPlugin> {
self.ensure_fresh();
self.data.read().unwrap().entries.get(id).cloned()
self.data
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.entries
.get(id)
.cloned()
}

fn mark_dirty(&self) {
Expand All @@ -210,7 +219,10 @@ impl PluginCache {

fn ensure_fresh(&self) {
let needs_reload = self.dirty.swap(false, Ordering::SeqCst) || {
let data = self.data.read().unwrap();
let data = self
.data
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner());
!data.loaded
};
if needs_reload {
Expand Down Expand Up @@ -282,7 +294,10 @@ impl PluginCache {
}

summaries.sort_by_key(|a| a.name.to_lowercase());
let mut data = self.data.write().unwrap();
let mut data = self
.data
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner());
data.list = summaries;
data.entries = entries;
data.loaded = true;
Expand Down Expand Up @@ -310,7 +325,10 @@ impl PluginCache {
}
}

let mut guard = self.watcher.lock().unwrap();
let mut guard = self
.watcher
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*guard = Some(watcher);
}
}
Expand Down
50 changes: 1 addition & 49 deletions Backend/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,44 +18,6 @@ use serde::{Deserialize, Serialize};
/// Default number of recent repositories stored when settings are missing or invalid.
pub const MAX_RECENTS: usize = 10;

/// Applies Git SSH-related environment variables from current settings.
///
/// # Parameters
/// - `cfg`: Current app configuration.
///
/// # Returns
/// - `()`.
fn apply_git_ssh_env(cfg: &AppConfig) {
// Prefer config-driven runtime env so the VCS backend (in another crate) can read it.
// Keep env var names stable for packaging and troubleshooting.
unsafe {
// Safety: OpenVCS sets these env vars during startup/config updates and treats them as
// process-wide configuration for child processes (e.g. `git`).
std::env::set_var(
"OPENVCS_SSH_MODE",
match cfg.git.ssh_binary {
crate::settings::GitSshBinary::Auto => "auto",
crate::settings::GitSshBinary::Host => "host",
crate::settings::GitSshBinary::Bundled => "bundled",
crate::settings::GitSshBinary::Custom => "custom",
},
);
}
if cfg.git.ssh_binary == crate::settings::GitSshBinary::Custom
&& !cfg.git.ssh_path.trim().is_empty()
{
unsafe {
// Safety: see comment above.
std::env::set_var("OPENVCS_SSH", cfg.git.ssh_path.trim());
}
} else {
unsafe {
// Safety: see comment above.
std::env::remove_var("OPENVCS_SSH");
}
}
}

/// Central application state.
/// Keeps track of the currently open repo and MRU recents.
/// Backend choice is tied to each repo (via `Repo::id()`), not stored globally.
Expand Down Expand Up @@ -84,10 +46,9 @@ impl AppState {
/// Creates app state by loading persisted settings and recent repositories.
///
/// # Returns
/// - A fully initialized [`AppState`] with config, recents, and runtime env applied.
/// - A fully initialized [`AppState`] with config and recent repositories loaded.
pub fn new_with_config() -> Self {
let cfg = AppConfig::load_or_default(); // reads ~/.config/openvcs/openvcs.conf
apply_git_ssh_env(&cfg);
let s = Self {
config: RwLock::new(cfg),
repo_config: RwLock::new(RepoConfig::default()),
Expand Down Expand Up @@ -125,7 +86,6 @@ impl AppState {
next.migrate();
next.validate();
next.save().map_err(|e| e.to_string())?;
apply_git_ssh_env(&next);
crate::monitoring::sync_backend_monitoring(&next);
*self.config.write() = next;
self.enforce_recents_limit_and_persist();
Expand All @@ -134,14 +94,6 @@ impl AppState {

/* -------- repo config -------- */

/// Returns a snapshot of repository-local settings.
///
/// # Returns
/// - A cloned [`RepoConfig`] for the current repository context.
pub fn repo_config(&self) -> RepoConfig {
self.repo_config.read().clone()
}

/// Replaces repository-local settings kept in memory.
///
/// # Parameters
Expand Down
Loading
Loading