Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 7 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,11 +26,12 @@ 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
Expand All @@ -43,11 +44,12 @@ 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_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
Expand Down
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
42 changes: 1 addition & 41 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 Down
2 changes: 1 addition & 1 deletion Frontend/src/scripts/features/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function openAbout(): Promise<void> {
if (!modal) return;

try {
const info = (TAURI.has ? await TAURI.invoke("about_info").catch(() => null) : null) as
const info = (await TAURI.invoke("about_info").catch(() => null)) as
| {
version?: string;
build?: string;
Expand Down
9 changes: 4 additions & 5 deletions Frontend/src/scripts/features/branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ function syncBranchLabelsFromState() {
/* ---------------- data load ---------------- */

async function loadBranches() {
if (!TAURI.has) return;
try {
const branches = await TAURI.invoke<Branch[]>('git_list_branches');
state.branches = Array.isArray(branches) ? branches : [];
Expand Down Expand Up @@ -174,7 +173,7 @@ export function bindBranchUI() {
return;
}
try {
if (TAURI.has) await TAURI.invoke('git_checkout_branch', { name });
await TAURI.invoke('git_checkout_branch', { name });
await runHook('onSwitchBranch', hookData);
await loadBranches(); // resync from backend instead of manual toggles
if (options.closePopover) closeBranchPopover();
Expand Down Expand Up @@ -214,7 +213,7 @@ export function bindBranchUI() {
const ok = await confirmBool(`Merge '${name}' into '${cur}'?`);
if (!ok) return;
try {
if (TAURI.has) await TAURI.invoke('git_merge_branch', { name });
await TAURI.invoke('git_merge_branch', { name });
notify(`Merged branch '${name}' into '${cur}'`);
await Promise.allSettled([renderList(), loadBranches()]);
} catch (e) {
Expand Down Expand Up @@ -263,7 +262,7 @@ export function bindBranchUI() {
notify(pre.reason || 'Delete cancelled');
return;
}
if (TAURI.has) await TAURI.invoke('git_delete_branch', { name, force: wantForce });
await TAURI.invoke('git_delete_branch', { name, force: wantForce });
await runHook('onBranchDelete', hookData);
notify(`${wantForce ? 'Force-deleted' : 'Deleted'} '${name}'`);
await loadBranches();
Expand All @@ -286,7 +285,7 @@ export function bindBranchUI() {
notify(pre.reason || 'Delete cancelled');
return;
}
if (TAURI.has) await TAURI.invoke('git_delete_branch', { name, force: true });
await TAURI.invoke('git_delete_branch', { name, force: true });
await runHook('onBranchDelete', hookData);
notify(`Force-deleted '${name}'`);
await loadBranches();
Expand Down
9 changes: 0 additions & 9 deletions Frontend/src/scripts/features/cherryPick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ export function wireCherryPick() {
const branch = (branchEl?.value || '').trim();
if (!commit || !branch) return;
try {
if (!TAURI.has) {
notify('Cherry-pick requires the desktop app');
return;
}
await TAURI.invoke('git_cherry_pick_to_branch', { id: commit, branch });
notify(`Cherry-picked onto ${branch}`);
closeModal('cherry-pick-modal');
Expand Down Expand Up @@ -74,11 +70,6 @@ export async function openCherryPick(commit: CommitLike) {
hydrate('cherry-pick-modal');
wireCherryPick();

if (!TAURI.has) {
notify('Cherry-pick requires the desktop app');
return;
}

await hydrateBranches();
const branches = (state.branches || [])
.filter((b: any) => {
Expand Down
8 changes: 2 additions & 6 deletions Frontend/src/scripts/features/commandSheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ function setDisabled(id: string, on: boolean) {
}

async function validateClone() {
if (!TAURI.has) return;
const url = cloneUrl?.value.trim();
const dest = clonePath?.value.trim();
try {
Expand All @@ -48,7 +47,6 @@ async function validateClone() {
}

async function validateAdd() {
if (!TAURI.has) return;
const path = addPath?.value.trim();
try {
const res = await TAURI.invoke<{ ok: boolean; reason?: string }>("validate_add_path", { path });
Expand Down Expand Up @@ -186,7 +184,6 @@ export function bindCommandSheet() {

// Browse buttons
el<HTMLButtonElement>("#browse-clone", root)?.addEventListener("click", async () => {
if (!TAURI.has) return;
try {
const dir = await TAURI.invoke<string>("browse_directory", { purpose: "clone_dest" });
if (dir && clonePath) {
Expand All @@ -197,7 +194,6 @@ export function bindCommandSheet() {
});

el<HTMLButtonElement>("#browse-add", root)?.addEventListener("click", async () => {
if (!TAURI.has) return;
try {
const dir = await TAURI.invoke<string>("browse_directory", { purpose: "add_repo" });
if (dir && addPath) {
Expand All @@ -213,7 +209,7 @@ export function bindCommandSheet() {
const dest = clonePath?.value.trim();
if (!url || !dest) return;
try {
if (TAURI.has) await TAURI.invoke("clone_repo", { url, dest });
await TAURI.invoke("clone_repo", { url, dest });
await refreshRepoSummary(); // ensure state + event
notify(`Cloned ${url} → ${dest}`);
closeSheet();
Expand All @@ -226,7 +222,7 @@ export function bindCommandSheet() {
const path = addPath?.value.trim();
if (!path) return;
try {
if (TAURI.has) await TAURI.invoke("add_repo", { path });
await TAURI.invoke("add_repo", { path });
await refreshRepoSummary(); // ensure state + event
notify(`Added ${path}`);
closeSheet();
Expand Down
9 changes: 1 addition & 8 deletions Frontend/src/scripts/features/conflicts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ async function ensureMergeModal() {

const applyBtn = modal.querySelector<HTMLButtonElement>('#merge-apply');
applyBtn?.addEventListener('click', async () => {
if (!TAURI.has) { notify('Saving merges requires the desktop app.'); return; }
if (!currentConflict) { notify('No conflict selected.'); return; }
const textarea = modal.querySelector<HTMLTextAreaElement>('#merge-result');
const content = textarea?.value ?? '';
Expand Down Expand Up @@ -85,7 +84,6 @@ async function ensureSummaryModal() {
const contBtn = modal.querySelector<HTMLButtonElement>('#conflicts-continue');

abortBtn?.addEventListener('click', async () => {
if (!TAURI.has) return;
const ok = await confirmBool('Abort the merge? This will discard merge progress.');
if (!ok) return;
try {
Expand All @@ -99,7 +97,6 @@ async function ensureSummaryModal() {
});

contBtn?.addEventListener('click', async () => {
if (!TAURI.has) return;
try {
await TAURI.invoke('git_merge_continue');
notify('Merge committed');
Expand All @@ -114,7 +111,6 @@ async function ensureSummaryModal() {
}

export async function openConflictsSummary(files: FileStatus[]): Promise<void> {
if (!TAURI.has) return;
await ensureSummaryModal();
const modal = document.getElementById('conflicts-summary-modal') as HTMLElement | null;
if (!modal) return;
Expand Down Expand Up @@ -207,7 +203,6 @@ export async function openConflictsSummary(files: FileStatus[]): Promise<void> {
}

export async function autoOpenFirstConflict(files: FileStatus[]): Promise<void> {
if (!TAURI.has) return;
if (!Array.isArray(files) || files.length === 0) return;

const conflicted = files.find((f) => String(f?.status || '').toUpperCase() === 'U' && !!f?.path);
Expand All @@ -230,7 +225,7 @@ export async function autoOpenFirstConflict(files: FileStatus[]): Promise<void>
}

async function ensureExternalMergeConfig() {
if (externalToolState.loaded || !TAURI.has) return;
if (externalToolState.loaded) return;
try {
const cfg = await TAURI.invoke<GlobalSettings>('get_global_settings');
const tool = cfg?.diff?.external_merge;
Expand All @@ -244,13 +239,11 @@ async function ensureExternalMergeConfig() {
}

export async function hasExternalMergeTool(): Promise<boolean> {
if (!TAURI.has) return false;
await ensureExternalMergeConfig();
return externalToolState.enabled;
}

export async function launchExternalMergeTool(path: string): Promise<void> {
if (!TAURI.has) { notify('Launching merge tools requires the desktop app.'); return; }
if (!(await hasExternalMergeTool())) {
notify('No custom merge tool configured');
return;
Expand Down
57 changes: 28 additions & 29 deletions Frontend/src/scripts/features/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,37 +50,36 @@ export function bindCommit() {
const selLines = linesMap[path] || {};
combinedPatch += buildPatchForSelected(path, lines, selHunks, selLines) + '\n';
}
if (TAURI.has) {
if (combinedPatch.trim().length > 0 || selectedFiles.length > 0) {
const hookData = {
summary,
description,
branch: state.branch,
files: selectedFiles,
stagedFiles: stagePaths,
partialFiles,
patch: combinedPatch,
};
const pre = await runHook('preCommit', hookData);
if (pre.cancelled) {
notify(pre.reason || 'Commit cancelled');
clearBusy('Ready');
return;
}
summary = String(hookData.summary || '').trim() || summary;
description = String(hookData.description || '');
await TAURI.invoke('commit_patch_and_files', {
summary,
description,
patch: combinedPatch,
files: selectedFiles,
stagePaths,
});
await runHook('onCommit', hookData);
} else {
notify('Select files or hunks to commit');
if (combinedPatch.trim().length > 0 || selectedFiles.length > 0) {
const hookData = {
summary,
description,
branch: state.branch,
files: selectedFiles,
stagedFiles: stagePaths,
partialFiles,
patch: combinedPatch,
};
const pre = await runHook('preCommit', hookData);
if (pre.cancelled) {
notify(pre.reason || 'Commit cancelled');
clearBusy('Ready');
return;
}
summary = String(hookData.summary || '').trim() || summary;
description = String(hookData.description || '');
await TAURI.invoke('commit_patch_and_files', {
summary,
description,
patch: combinedPatch,
files: selectedFiles,
stagePaths,
});
await runHook('onCommit', hookData);
}
else {
notify('Select files or hunks to commit');
return;
}
notify(`Committed to ${state.branch}: ${summary}`);
if (commitSummary) commitSummary.value = '';
Expand Down
2 changes: 1 addition & 1 deletion Frontend/src/scripts/features/newBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export function wireNewBranch() {
return;
}
}
if (TAURI.has) await TAURI.invoke('git_create_branch', { name, from, checkout });
await TAURI.invoke('git_create_branch', { name, from, checkout });
await runHook('onBranchCreate', hookData);
if (checkout) {
await runHook('onSwitchBranch', { from: state.branch, to: name });
Expand Down
Loading
Loading