From a4c722892e627060e6749dcf3476a9d02e3dc712 Mon Sep 17 00:00:00 2001 From: Jordon Date: Mon, 27 Apr 2026 17:36:22 +0100 Subject: [PATCH 01/11] Rename functions to be more agnostic --- Backend/src/lib.rs | 78 +++++++++---------- Backend/src/tauri_commands/branches.rs | 66 ++++++++-------- Backend/src/tauri_commands/commit.rs | 24 +++--- Backend/src/tauri_commands/conflicts.rs | 62 +++++++-------- Backend/src/tauri_commands/general.rs | 6 +- Backend/src/tauri_commands/remotes.rs | 54 ++++++------- Backend/src/tauri_commands/repo_files.rs | 6 +- Backend/src/tauri_commands/shared.rs | 4 +- Backend/src/tauri_commands/stash.rs | 36 ++++----- Backend/src/tauri_commands/status.rs | 30 +++---- Backend/src/validate.rs | 26 +++---- Frontend/src/scripts/features/branches.ts | 12 +-- Frontend/src/scripts/features/cherryPick.ts | 2 +- Frontend/src/scripts/features/conflicts.ts | 12 +-- Frontend/src/scripts/features/diff.ts | 2 +- Frontend/src/scripts/features/newBranch.ts | 2 +- Frontend/src/scripts/features/renameBranch.ts | 2 +- .../src/scripts/features/repo/diffView.ts | 20 ++--- Frontend/src/scripts/features/repo/history.ts | 10 +-- Frontend/src/scripts/features/repo/hydrate.ts | 19 +++-- .../src/scripts/features/repo/interactions.ts | 6 +- Frontend/src/scripts/features/repo/stash.ts | 18 ++--- Frontend/src/scripts/features/repoSettings.ts | 2 +- Frontend/src/scripts/features/setUpstream.ts | 2 +- Frontend/src/scripts/features/sshAuth.ts | 2 +- Frontend/src/scripts/features/sshHostkey.ts | 2 +- Frontend/src/scripts/features/stashConfirm.ts | 4 +- Frontend/src/scripts/main.ts | 14 ++-- 28 files changed, 263 insertions(+), 260 deletions(-) diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 5dc52c7..5efa15e 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -316,56 +316,56 @@ fn build_invoke_handler( tauri_commands::list_vcs_backends_cmd, tauri_commands::set_vcs_backend_cmd, tauri_commands::reopen_current_repo_cmd, - tauri_commands::validate_git_url, + tauri_commands::validate_vcs_url, tauri_commands::validate_add_path, tauri_commands::validate_clone_input, tauri_commands::current_repo_path, tauri_commands::list_recent_repos, - tauri_commands::git_list_branches, - tauri_commands::git_status, - tauri_commands::git_log, - tauri_commands::git_stash_list, - tauri_commands::git_stash_push, - tauri_commands::git_stash_apply, - tauri_commands::git_stash_pop, - tauri_commands::git_stash_drop, - tauri_commands::git_stash_show, - tauri_commands::git_head_status, - tauri_commands::git_checkout_branch, - tauri_commands::git_create_branch, - tauri_commands::git_rename_branch, - tauri_commands::git_current_branch, + tauri_commands::vcs_list_branches, + tauri_commands::vcs_status, + tauri_commands::vcs_log, + tauri_commands::vcs_stash_list, + tauri_commands::vcs_stash_push, + tauri_commands::vcs_stash_apply, + tauri_commands::vcs_stash_pop, + tauri_commands::vcs_stash_drop, + tauri_commands::vcs_stash_show, + tauri_commands::vcs_head_status, + tauri_commands::vcs_checkout_branch, + tauri_commands::vcs_create_branch, + tauri_commands::vcs_rename_branch, + tauri_commands::vcs_current_branch, tauri_commands::get_repo_summary, tauri_commands::open_repo, tauri_commands::clone_repo, - tauri_commands::git_diff_file, - tauri_commands::git_conflict_details, - tauri_commands::git_resolve_conflict_side, - tauri_commands::git_save_merge_result, - tauri_commands::git_launch_merge_tool, - tauri_commands::git_delete_branch, - tauri_commands::git_merge_branch, - tauri_commands::git_merge_context, - tauri_commands::git_merge_abort, - tauri_commands::git_merge_continue, - tauri_commands::git_set_upstream, - tauri_commands::git_diff_commit, - tauri_commands::git_cherry_pick_to_branch, - tauri_commands::git_revert_commit, + tauri_commands::vcs_diff_file, + tauri_commands::vcs_conflict_details, + tauri_commands::vcs_resolve_conflict_side, + tauri_commands::vcs_save_merge_result, + tauri_commands::vcs_launch_merge_tool, + tauri_commands::vcs_delete_branch, + tauri_commands::vcs_merge_branch, + tauri_commands::vcs_merge_context, + tauri_commands::vcs_merge_abort, + tauri_commands::vcs_merge_continue, + tauri_commands::vcs_set_upstream, + tauri_commands::vcs_diff_commit, + tauri_commands::vcs_cherry_pick_to_branch, + tauri_commands::vcs_revert_commit, tauri_commands::commit_changes, tauri_commands::commit_selected, tauri_commands::commit_patch, tauri_commands::commit_patch_and_files, - tauri_commands::git_discard_paths, - tauri_commands::git_discard_patch, - tauri_commands::git_set_remote_url, - tauri_commands::git_fetch, - tauri_commands::git_fetch_all, - tauri_commands::git_pull, - tauri_commands::git_push, - tauri_commands::git_undo_since_push, - tauri_commands::git_undo_to_commit, - tauri_commands::git_add_to_gitignore_paths, + tauri_commands::vcs_discard_paths, + tauri_commands::vcs_discard_patch, + tauri_commands::vcs_set_remote_url, + tauri_commands::vcs_fetch, + tauri_commands::vcs_fetch_all, + tauri_commands::vcs_pull, + tauri_commands::vcs_push, + tauri_commands::vcs_undo_since_push, + tauri_commands::vcs_undo_to_commit, + tauri_commands::vcs_add_to_gitignore_paths, tauri_commands::open_repo_file, tauri_commands::read_repo_file_text, tauri_commands::list_themes, diff --git a/Backend/src/tauri_commands/branches.rs b/Backend/src/tauri_commands/branches.rs index d8faf9f..667d418 100644 --- a/Backend/src/tauri_commands/branches.rs +++ b/Backend/src/tauri_commands/branches.rs @@ -135,9 +135,9 @@ fn backend_merge_message_template(backend_id: &BackendId) -> String { /// # Returns /// - `Ok(Vec)` sorted branch list. /// - `Err(String)` when repository access fails. -pub async fn git_list_branches(state: State<'_, AppState>) -> Result, String> { +pub async fn vcs_list_branches(state: State<'_, AppState>) -> Result, String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_list_branches", repo, move |repo| { + run_repo_task("vcs_list_branches", repo, move |repo| { info!("list_branches: fetching unified branches via Vcs::branches()"); let vcs = repo.inner(); debug!("list_branches: workdir={}", vcs.workdir().display()); @@ -256,11 +256,11 @@ pub struct HeadStatus { /// # Returns /// - `Ok(HeadStatus)` with branch and commit data. /// - `Err(String)` when repository queries fail. -pub async fn git_head_status(state: State<'_, AppState>) -> Result { +pub async fn vcs_head_status(state: State<'_, AppState>) -> Result { use crate::core::models::LogQuery; let repo = current_repo_or_err(&state)?; - run_repo_task("git_head_status", repo, move |repo| { + run_repo_task("vcs_head_status", repo, move |repo| { let branch = repo.inner().current_branch().map_err(|e| e.to_string())?; let q = LogQuery { rev: Some("HEAD".into()), @@ -289,23 +289,23 @@ pub async fn git_head_status(state: State<'_, AppState>) -> Result, name: String) -> Result<(), String> { +pub async fn vcs_checkout_branch(state: State<'_, AppState>, name: String) -> Result<(), String> { let branch = name.trim(); if branch.is_empty() { return Err("Branch name cannot be empty".to_string()); } - info!("git_checkout_branch: attempting to checkout '{branch}'"); + info!("vcs_checkout_branch: attempting to checkout '{branch}'"); let repo = current_repo_or_err(&state)?; let branch = branch.to_string(); - run_repo_task("git_checkout_branch", repo, move |repo| { + run_repo_task("vcs_checkout_branch", repo, move |repo| { repo.inner().checkout_branch(&branch).map_err(|e| { - error!("git_checkout_branch: failed to checkout '{}': {e}", branch); + error!("vcs_checkout_branch: failed to checkout '{}': {e}", branch); e.to_string() })?; - info!("git_checkout_branch: successfully checked out '{}'", branch); + info!("vcs_checkout_branch: successfully checked out '{}'", branch); Ok(()) }) .await @@ -322,7 +322,7 @@ pub async fn git_checkout_branch(state: State<'_, AppState>, name: String) -> Re /// # Returns /// - `Ok(())` when deletion succeeds. /// - `Err(String)` when validation or deletion fails. -pub async fn git_delete_branch( +pub async fn vcs_delete_branch( state: State<'_, AppState>, name: String, force: Option, @@ -334,7 +334,7 @@ pub async fn git_delete_branch( let repo = current_repo_or_err(&state)?; let force = force.unwrap_or(false); let branch = name.to_string(); - run_repo_task("git_delete_branch", repo, move |repo| { + run_repo_task("vcs_delete_branch", repo, move |repo| { repo.inner() .delete_branch(&branch, force) .map_err(|e| e.to_string()) @@ -353,7 +353,7 @@ pub async fn git_delete_branch( /// # Returns /// - `Ok(())` when rename succeeds. /// - `Err(String)` when validation or rename fails. -pub async fn git_rename_branch( +pub async fn vcs_rename_branch( state: State<'_, AppState>, old_name: String, new_name: String, @@ -369,7 +369,7 @@ pub async fn git_rename_branch( let repo = current_repo_or_err(&state)?; let old = old.to_string(); let newn = newn.to_string(); - run_repo_task("git_rename_branch", repo, move |repo| { + run_repo_task("vcs_rename_branch", repo, move |repo| { repo.inner() .rename_branch(&old, &newn) .map_err(|e| e.to_string()) @@ -387,7 +387,7 @@ pub async fn git_rename_branch( /// # Returns /// - `Ok(())` when merge succeeds. /// - `Err(String)` when validation or merge fails. -pub async fn git_merge_branch(state: State<'_, AppState>, name: String) -> Result<(), String> { +pub async fn vcs_merge_branch(state: State<'_, AppState>, name: String) -> Result<(), String> { let name = name.trim(); if name.is_empty() { return Err("Branch name cannot be empty".to_string()); @@ -395,7 +395,7 @@ pub async fn git_merge_branch(state: State<'_, AppState>, name: String) -> Resul let repo = current_repo_or_err(&state)?; let branch = name.to_string(); let template = backend_merge_message_template(&repo.id()); - run_repo_task("git_merge_branch", repo, move |repo| { + run_repo_task("vcs_merge_branch", repo, move |repo| { let vcs = repo.inner(); let target_branch = vcs .current_branch() @@ -459,9 +459,9 @@ pub struct MergeContext { /// # Returns /// - `Ok(MergeContext)` merge state payload. /// - `Err(String)` when repository access fails. -pub async fn git_merge_context(state: State<'_, AppState>) -> Result { +pub async fn vcs_merge_context(state: State<'_, AppState>) -> Result { let repo = current_repo_or_err(&state)?; - run_repo_task("git_merge_context", repo, move |repo| { + run_repo_task("vcs_merge_context", repo, move |repo| { let in_progress = repo.inner().merge_in_progress().unwrap_or(false); Ok(MergeContext { in_progress }) }) @@ -477,9 +477,9 @@ pub async fn git_merge_context(state: State<'_, AppState>) -> Result) -> Result<(), String> { +pub async fn vcs_merge_abort(state: State<'_, AppState>) -> Result<(), String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_merge_abort", repo, move |repo| { + run_repo_task("vcs_merge_abort", repo, move |repo| { repo.inner().merge_abort().map_err(|e| e.to_string()) }) .await @@ -494,9 +494,9 @@ pub async fn git_merge_abort(state: State<'_, AppState>) -> Result<(), String> { /// # Returns /// - `Ok(())` when continuation succeeds. /// - `Err(String)` when continuation fails. -pub async fn git_merge_continue(state: State<'_, AppState>) -> Result<(), String> { +pub async fn vcs_merge_continue(state: State<'_, AppState>) -> Result<(), String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_merge_continue", repo, move |repo| { + run_repo_task("vcs_merge_continue", repo, move |repo| { repo.inner().merge_continue().map_err(|e| e.to_string()) }) .await @@ -513,7 +513,7 @@ pub async fn git_merge_continue(state: State<'_, AppState>) -> Result<(), String /// # Returns /// - `Ok(())` when upstream is updated. /// - `Err(String)` when validation or update fails. -pub async fn git_set_upstream( +pub async fn vcs_set_upstream( state: State<'_, AppState>, branch: String, upstream: String, @@ -527,7 +527,7 @@ pub async fn git_set_upstream( let repo = current_repo_or_err(&state)?; let branch = branch.to_string(); let upstream = upstream.to_string(); - run_repo_task("git_set_upstream", repo, move |repo| { + run_repo_task("vcs_set_upstream", repo, move |repo| { repo.inner() .set_branch_upstream(&branch, &upstream) .map_err(|e| e.to_string()) @@ -547,14 +547,14 @@ pub async fn git_set_upstream( /// # Returns /// - `Ok(())` when creation succeeds. /// - `Err(String)` when backend operations fail. -pub async fn git_create_branch( +pub async fn vcs_create_branch( state: State<'_, AppState>, name: String, from: Option, checkout: Option, ) -> Result<(), String> { info!( - "git_create_branch: requested branch '{}', from={:?}, checkout={:?}", + "vcs_create_branch: requested branch '{}', from={:?}, checkout={:?}", name, from, checkout ); @@ -562,14 +562,14 @@ pub async fn git_create_branch( let checkout_flag = checkout.unwrap_or(false); let branch_name = name.clone(); let from_branch = from.map(|s| s.to_string()); - run_repo_task("git_create_branch", repo, move |repo| { + run_repo_task("vcs_create_branch", repo, move |repo| { let vcs = repo.inner(); if let Some(from) = from_branch.as_ref() { match vcs.checkout_branch(from) { - Ok(_) => info!("git_create_branch: successfully checked out base branch '{from}'"), + Ok(_) => info!("vcs_create_branch: successfully checked out base branch '{from}'"), Err(e) => { - error!("git_create_branch: failed to checkout base branch '{from}': {e}"); + error!("vcs_create_branch: failed to checkout base branch '{from}': {e}"); return Err(format!("base branch not found or cannot checkout: {e}")); } } @@ -577,11 +577,11 @@ pub async fn git_create_branch( vcs.create_branch(&branch_name, checkout_flag) .map_err(|e| { - error!("git_create_branch: failed to create branch '{branch_name}': {e}"); + error!("vcs_create_branch: failed to create branch '{branch_name}': {e}"); e.to_string() })?; - info!("git_create_branch: successfully created branch '{branch_name}'"); + info!("vcs_create_branch: successfully created branch '{branch_name}'"); Ok(()) }) .await @@ -620,7 +620,7 @@ pub async fn get_repo_summary(state: State<'_, AppState>) -> Result) -> Result) -> Result { +pub async fn vcs_current_branch(state: State<'_, AppState>) -> Result { let repo = current_repo_or_err(&state)?; - run_repo_task("git_current_branch", repo, move |repo| { + run_repo_task("vcs_current_branch", repo, move |repo| { repo.inner() .current_branch() .map_err(|e| e.to_string())? diff --git a/Backend/src/tauri_commands/commit.rs b/Backend/src/tauri_commands/commit.rs index f4ac748..3a9d388 100644 --- a/Backend/src/tauri_commands/commit.rs +++ b/Backend/src/tauri_commands/commit.rs @@ -11,20 +11,20 @@ use crate::state::AppState; use super::{current_repo_or_err, progress_bridge, run_repo_task}; -/// Resolves the repository commit identity from Git config. +/// Resolves the repository commit identity from VCS config. /// /// # Parameters /// - `repo`: Active repository handle. /// /// # Returns -/// - `Ok((name, email))` when Git has a configured identity. +/// - `Ok((name, email))` when the repository has a configured identity. /// - `Err(String)` when the repository has no usable commit identity. fn commit_identity(repo: &Repo) -> Result<(String, String), String> { repo.inner() .get_identity() .map_err(|e| e.to_string())? .ok_or_else(|| { - "No Git commit identity configured for this repository; set user.name and user.email in Git".to_string() + "No VCS commit identity configured for this repository; set user.name and user.email in the repository settings".to_string() }) } @@ -323,20 +323,20 @@ pub async fn commit_patch_and_files( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on validation or cherry-pick failure. -pub async fn git_cherry_pick_to_branch( +pub async fn vcs_cherry_pick_to_branch( window: Window, state: State<'_, AppState>, id: String, branch: String, ) -> Result<(), String> { info!( - "git_cherry_pick_to_branch called (id={}, branch={})", + "vcs_cherry_pick_to_branch called (id={}, branch={})", id, branch ); let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - run_repo_task("git_cherry_pick_to_branch", repo, move |repo| { + run_repo_task("vcs_cherry_pick_to_branch", repo, move |repo| { let id = id.trim().to_string(); let branch = branch.trim().to_string(); if id.is_empty() { @@ -348,7 +348,7 @@ pub async fn git_cherry_pick_to_branch( let on = progress_bridge(app); on(VcsEvent::Progress { - phase: "git".into(), + phase: "vcs".into(), detail: format!("Checking out '{branch}'…"), }); repo.inner() @@ -356,7 +356,7 @@ pub async fn git_cherry_pick_to_branch( .map_err(|e| e.to_string())?; on(VcsEvent::Progress { - phase: "git".into(), + phase: "vcs".into(), detail: format!("Cherry-picking {id}…"), }); repo.inner().cherry_pick(&id).map_err(|e| e.to_string())?; @@ -380,16 +380,16 @@ pub async fn git_cherry_pick_to_branch( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on validation or revert failure. -pub async fn git_revert_commit( +pub async fn vcs_revert_commit( window: Window, state: State<'_, AppState>, id: String, ) -> Result<(), String> { - info!("git_revert_commit called (id={})", id); + info!("vcs_revert_commit called (id={})", id); let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - run_repo_task("git_revert_commit", repo, move |repo| { + run_repo_task("vcs_revert_commit", repo, move |repo| { let id = id.trim().to_string(); if id.is_empty() { return Err("Commit id cannot be empty".into()); @@ -397,7 +397,7 @@ pub async fn git_revert_commit( let on = progress_bridge(app); on(VcsEvent::Progress { - phase: "git".into(), + phase: "vcs".into(), detail: format!("Reverting {id}…"), }); repo.inner() diff --git a/Backend/src/tauri_commands/conflicts.rs b/Backend/src/tauri_commands/conflicts.rs index fbf8541..2d6fa8a 100644 --- a/Backend/src/tauri_commands/conflicts.rs +++ b/Backend/src/tauri_commands/conflicts.rs @@ -23,20 +23,20 @@ use super::{current_repo_or_err, run_repo_task}; /// # Returns /// - `Ok(ConflictDetails)` with conflict metadata/content. /// - `Err(String)` when lookup fails. -pub async fn git_conflict_details( +pub async fn vcs_conflict_details( state: State<'_, AppState>, path: String, ) -> Result { let start = std::time::Instant::now(); - info!("git_conflict_details: path='{}'", path); + info!("vcs_conflict_details: path='{}'", path); let repo = current_repo_or_err(&state)?; let path_clone = path.clone(); - let result = run_repo_task("git_conflict_details", repo, move |repo| { + let result = run_repo_task("vcs_conflict_details", repo, move |repo| { repo.inner() .conflict_details(&PathBuf::from(&path)) .map_err(|e| { - error!("git_conflict_details: failed for '{}': {}", path, e); + error!("vcs_conflict_details: failed for '{}': {}", path, e); e.to_string() }) }) @@ -45,14 +45,14 @@ pub async fn git_conflict_details( match &result { Ok(details) => { debug!( - "git_conflict_details: found conflict details for '{}' ({:?})", + "vcs_conflict_details: found conflict details for '{}' ({:?})", path_clone, start.elapsed() ); - trace!("git_conflict_details: binary={}", details.binary); + trace!("vcs_conflict_details: binary={}", details.binary); } Err(e) => { - error!("git_conflict_details: failed: {}", e); + error!("vcs_conflict_details: failed: {}", e); } } @@ -70,33 +70,33 @@ pub async fn git_conflict_details( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on validation or checkout failure. -pub async fn git_resolve_conflict_side( +pub async fn vcs_resolve_conflict_side( state: State<'_, AppState>, path: String, side: String, ) -> Result<(), String> { let start = std::time::Instant::now(); info!( - "git_resolve_conflict_side: path='{}', side='{}'", + "vcs_resolve_conflict_side: path='{}', side='{}'", path, side ); let repo = current_repo_or_err(&state)?; let path_clone = path.clone(); let side_clone = side.clone(); - let result = run_repo_task("git_resolve_conflict_side", repo, move |repo| { + let result = run_repo_task("vcs_resolve_conflict_side", repo, move |repo| { let which = match side.to_lowercase().as_str() { "ours" => ConflictSide::Ours, "theirs" => ConflictSide::Theirs, other => { - warn!("git_resolve_conflict_side: invalid side '{}'", other); + warn!("vcs_resolve_conflict_side: invalid side '{}'", other); return Err(format!("invalid conflict side '{other}'")); } }; repo.inner() .checkout_conflict_side(&PathBuf::from(&path), which) .map_err(|e| { - error!("git_resolve_conflict_side: failed for '{}': {}", path, e); + error!("vcs_resolve_conflict_side: failed for '{}': {}", path, e); e.to_string() }) }) @@ -105,14 +105,14 @@ pub async fn git_resolve_conflict_side( match &result { Ok(()) => { debug!( - "git_resolve_conflict_side: resolved '{}' with '{}' ({:?})", + "vcs_resolve_conflict_side: resolved '{}' with '{}' ({:?})", path_clone, side_clone, start.elapsed() ); } Err(e) => { - error!("git_resolve_conflict_side: failed: {}", e); + error!("vcs_resolve_conflict_side: failed: {}", e); } } @@ -130,25 +130,25 @@ pub async fn git_resolve_conflict_side( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` when write/save fails. -pub async fn git_save_merge_result( +pub async fn vcs_save_merge_result( state: State<'_, AppState>, path: String, content: String, ) -> Result<(), String> { let start = std::time::Instant::now(); info!( - "git_save_merge_result: path='{}', content_len={}", + "vcs_save_merge_result: path='{}', content_len={}", path, content.len() ); let repo = current_repo_or_err(&state)?; let path_clone = path.clone(); - let result = run_repo_task("git_save_merge_result", repo, move |repo| { + let result = run_repo_task("vcs_save_merge_result", repo, move |repo| { repo.inner() .write_merge_result(&PathBuf::from(&path), content.as_bytes()) .map_err(|e| { - error!("git_save_merge_result: failed for '{}': {}", path, e); + error!("vcs_save_merge_result: failed for '{}': {}", path, e); e.to_string() }) }) @@ -157,13 +157,13 @@ pub async fn git_save_merge_result( match &result { Ok(()) => { debug!( - "git_save_merge_result: saved '{}' ({:?})", + "vcs_save_merge_result: saved '{}' ({:?})", path_clone, start.elapsed() ); } Err(e) => { - error!("git_save_merge_result: failed: {}", e); + error!("vcs_save_merge_result: failed: {}", e); } } @@ -197,25 +197,25 @@ fn tool_args(tool: &ExternalTool) -> (String, Vec) { /// # Returns /// - `Ok(())` when the tool process is started. /// - `Err(String)` when tool config is missing or spawn fails. -pub async fn git_launch_merge_tool(state: State<'_, AppState>, path: String) -> Result<(), String> { +pub async fn vcs_launch_merge_tool(state: State<'_, AppState>, path: String) -> Result<(), String> { let start = std::time::Instant::now(); - info!("git_launch_merge_tool: path='{}'", path); + info!("vcs_launch_merge_tool: path='{}'", path); let cfg = state.config(); let tool = cfg.diff.external_merge.clone(); if !tool.enabled { - warn!("git_launch_merge_tool: external merge tool is disabled"); + warn!("vcs_launch_merge_tool: external merge tool is disabled"); return Err("no external merge tool configured".into()); } if tool.path.trim().is_empty() { - warn!("git_launch_merge_tool: no tool path configured"); + warn!("vcs_launch_merge_tool: no tool path configured"); return Err("no external merge tool configured".into()); } debug!( - "git_launch_merge_tool: tool='{}', args='{}'", + "vcs_launch_merge_tool: tool='{}', args='{}'", tool.path, tool.args ); @@ -225,7 +225,7 @@ pub async fn git_launch_merge_tool(state: State<'_, AppState>, path: String) -> let includes_placeholder = args_template.iter().any(|arg| arg.contains("{path}")); let path_for_log = path.clone(); - let result = run_repo_task("git_launch_merge_tool", repo, move |repo| { + let result = run_repo_task("vcs_launch_merge_tool", repo, move |repo| { let repo_root = repo.inner().workdir().to_path_buf(); let rel = PathBuf::from(&path); let abs = if rel.is_absolute() { @@ -235,7 +235,7 @@ pub async fn git_launch_merge_tool(state: State<'_, AppState>, path: String) -> }; trace!( - "git_launch_merge_tool: repo_root='{}', abs_path='{}'", + "vcs_launch_merge_tool: repo_root='{}', abs_path='{}'", repo_root.display(), abs.display() ); @@ -265,13 +265,13 @@ pub async fn git_launch_merge_tool(state: State<'_, AppState>, path: String) -> } debug!( - "git_launch_merge_tool: spawning '{}' with args {:?}", + "vcs_launch_merge_tool: spawning '{}' with args {:?}", tool_path, expanded ); cmd.spawn().map(|_| ()).map_err(|e| { error!( - "git_launch_merge_tool: failed to spawn '{}': {}", + "vcs_launch_merge_tool: failed to spawn '{}': {}", tool_path, e ); e.to_string() @@ -282,13 +282,13 @@ pub async fn git_launch_merge_tool(state: State<'_, AppState>, path: String) -> match &result { Ok(()) => { info!( - "git_launch_merge_tool: launched tool for '{}' ({:?})", + "vcs_launch_merge_tool: launched tool for '{}' ({:?})", path_for_log, start.elapsed() ); } Err(e) => { - error!("git_launch_merge_tool: failed: {}", e); + error!("vcs_launch_merge_tool: failed: {}", e); } } diff --git a/Backend/src/tauri_commands/general.rs b/Backend/src/tauri_commands/general.rs index c537677..6c236ff 100644 --- a/Backend/src/tauri_commands/general.rs +++ b/Backend/src/tauri_commands/general.rs @@ -267,15 +267,15 @@ pub async fn clone_repo( } #[tauri::command] -/// Validates a user-entered Git URL. +/// Validates a user-entered VCS URL. /// /// # Parameters /// - `url`: Candidate URL string. /// /// # Returns /// - Validation result describing whether the URL is acceptable. -pub fn validate_git_url(url: String) -> validate::Validation { - validate::validate_git_url(url) +pub fn validate_vcs_url(url: String) -> validate::Validation { + validate::validate_vcs_url(url) } #[tauri::command] diff --git a/Backend/src/tauri_commands/remotes.rs b/Backend/src/tauri_commands/remotes.rs index ff14c12..04216a7 100644 --- a/Backend/src/tauri_commands/remotes.rs +++ b/Backend/src/tauri_commands/remotes.rs @@ -188,7 +188,7 @@ struct SshAuthPrompt { /// # Returns /// - `Ok(())` when remote is set. /// - `Err(String)` on validation or backend failure. -pub async fn git_set_remote_url( +pub async fn vcs_set_remote_url( state: State<'_, AppState>, name: String, url: String, @@ -204,7 +204,7 @@ pub async fn git_set_remote_url( return Err("Remote URL cannot be empty".to_string()); } - run_repo_task("git_set_remote_url", repo, move |repo| { + run_repo_task("vcs_set_remote_url", repo, move |repo| { repo.inner() .ensure_remote(&name, &url) .map_err(|e| e.to_string())?; @@ -225,14 +225,14 @@ pub async fn git_set_remote_url( /// # Returns /// - `Ok(())` when fetch completes. /// - `Err(String)` when no repo/branch is selected or fetch fails. -pub async fn git_fetch( +pub async fn vcs_fetch( window: Window, state: State<'_, AppState>, ) -> Result<(), String> { let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - let current = run_repo_task("git_fetch", repo, move |repo| { - info!("git_fetch called"); + let current = run_repo_task("vcs_fetch", repo, move |repo| { + info!("vcs_fetch called"); let on = Some(progress_bridge(app.clone())); let current = repo .inner() @@ -276,7 +276,7 @@ pub async fn git_fetch( .await?; let _ = window.app_handle().emit( - "git-progress", + "vcs-progress", ProgressPayload { message: format!("Fetch complete ({current})"), }, @@ -294,14 +294,14 @@ pub async fn git_fetch( /// # Returns /// - `Ok(())` when all remotes fetch successfully. /// - `Err(String)` when one or more remotes fail. -pub async fn git_fetch_all( +pub async fn vcs_fetch_all( window: Window, state: State<'_, AppState>, ) -> Result<(), String> { let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - run_repo_task("git_fetch_all", repo, move |repo| { - info!("git_fetch_all called"); + run_repo_task("vcs_fetch_all", repo, move |repo| { + info!("vcs_fetch_all called"); let on = Some(progress_bridge(app.clone())); let remotes = repo.inner().list_remotes().map_err(|e| { error!("Failed to list remotes: {e}"); @@ -309,7 +309,7 @@ pub async fn git_fetch_all( })?; if log::log_enabled!(log::Level::Trace) { - log::trace!("git_fetch_all: remotes={:?}", remotes); + log::trace!("vcs_fetch_all: remotes={:?}", remotes); } let mut failures: Vec = Vec::new(); @@ -336,10 +336,10 @@ pub async fn git_fetch_all( match repo.inner().branches() { Ok(mut branches) => { branches.sort_by(|a, b| a.full_ref.cmp(&b.full_ref)); - log::trace!("git_fetch_all: branches() returned {} refs", branches.len()); + log::trace!("vcs_fetch_all: branches() returned {} refs", branches.len()); for b in branches { log::trace!( - "git_fetch_all: branch ref={} name={} kind={:?} current={}", + "vcs_fetch_all: branch ref={} name={} kind={:?} current={}", b.full_ref, b.name, b.kind, @@ -347,7 +347,7 @@ pub async fn git_fetch_all( ); } } - Err(e) => log::trace!("git_fetch_all: branches() failed: {e}"), + Err(e) => log::trace!("vcs_fetch_all: branches() failed: {e}"), } } @@ -370,14 +370,14 @@ pub async fn git_fetch_all( /// # Returns /// - `Ok(PullResult)` describing whether pull executed or was skipped. /// - `Err(String)` when pull fails. -pub async fn git_pull( +pub async fn vcs_pull( window: Window, state: State<'_, AppState>, ) -> Result { let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - let result = run_repo_task("git_pull", repo, move |repo| { - info!("git_pull called"); + let result = run_repo_task("vcs_pull", repo, move |repo| { + info!("vcs_pull called"); let on = Some(progress_bridge(app.clone())); let current = repo .inner() @@ -464,7 +464,7 @@ pub async fn git_pull( }; let _ = window .app_handle() - .emit("git-progress", ProgressPayload { message: msg }); + .emit("vcs-progress", ProgressPayload { message: msg }); Ok(result) } @@ -489,14 +489,14 @@ pub struct PullResult { /// # Returns /// - `Ok(())` when push completes. /// - `Err(String)` when push fails. -pub async fn git_push( +pub async fn vcs_push( window: Window, state: State<'_, AppState>, ) -> Result<(), String> { let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - let current = run_repo_task("git_push", repo, move |repo| { - info!("git_push called"); + let current = run_repo_task("vcs_push", repo, move |repo| { + info!("vcs_push called"); let on = Some(progress_bridge(app.clone())); let current = repo @@ -532,7 +532,7 @@ pub async fn git_push( .await?; let _ = window.app_handle().emit( - "git-progress", + "vcs-progress", ProgressPayload { message: format!("Push complete ({current})"), }, @@ -551,15 +551,15 @@ pub async fn git_push( /// # Returns /// - `Ok(())` when reset succeeds. /// - `Err(String)` when nothing is ahead or reset fails. -pub async fn git_undo_since_push( +pub async fn vcs_undo_since_push( window: Window, state: State<'_, AppState>, ) -> Result<(), String> { - info!("git_undo_since_push called"); + info!("vcs_undo_since_push called"); let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - run_repo_task("git_undo_since_push", repo, move |repo| { + run_repo_task("vcs_undo_since_push", repo, move |repo| { let status = repo.inner().status_payload().map_err(|e| e.to_string())?; if status.ahead == 0 { return Err("Nothing to undo (no unpushed commits)".into()); @@ -597,16 +597,16 @@ pub async fn git_undo_since_push( /// # Returns /// - `Ok(())` when reset succeeds. /// - `Err(String)` when validation or reset fails. -pub async fn git_undo_to_commit( +pub async fn vcs_undo_to_commit( window: Window, state: State<'_, AppState>, id: String, ) -> Result<(), String> { - info!("git_undo_to_commit called for {id}"); + info!("vcs_undo_to_commit called for {id}"); let repo = current_repo_or_err(&state)?; let app = window.app_handle().clone(); - run_repo_task("git_undo_to_commit", repo, move |repo| { + run_repo_task("vcs_undo_to_commit", repo, move |repo| { let mut ahead_list: Vec = Vec::new(); { let mut q = LogQuery::head(1000); diff --git a/Backend/src/tauri_commands/repo_files.rs b/Backend/src/tauri_commands/repo_files.rs index 7db4200..0fc49e0 100644 --- a/Backend/src/tauri_commands/repo_files.rs +++ b/Backend/src/tauri_commands/repo_files.rs @@ -70,17 +70,17 @@ fn normalize_gitignore_entry(path: &str) -> Result { /// # Returns /// - `Ok(())` when update succeeds. /// - `Err(String)` when validation or file IO fails. -pub async fn git_add_to_gitignore_paths( +pub async fn vcs_add_to_gitignore_paths( state: State<'_, AppState>, paths: Vec, ) -> Result<(), String> { - info!("git_add_to_gitignore_paths called (count={})", paths.len()); + info!("vcs_add_to_gitignore_paths called (count={})", paths.len()); if paths.is_empty() { return Ok(()); } let repo = current_repo_or_err(&state)?; - run_repo_task("git_add_to_gitignore_paths", repo, move |repo| { + run_repo_task("vcs_add_to_gitignore_paths", repo, move |repo| { let workdir = repo.inner().workdir(); let gitignore_path = workdir.join(".gitignore"); diff --git a/Backend/src/tauri_commands/shared.rs b/Backend/src/tauri_commands/shared.rs index b857038..0cadfc6 100644 --- a/Backend/src/tauri_commands/shared.rs +++ b/Backend/src/tauri_commands/shared.rs @@ -45,12 +45,12 @@ pub(crate) fn progress_bridge(app: AppHandle) -> OnEvent { }; let ts_ms = time::OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000; - let entry = OutputLogEntry::new(ts_ms as i64, level, "git", msg.clone()); + let entry = OutputLogEntry::new(ts_ms as i64, level, "vcs", msg.clone()); let state = app.state::(); state.push_output_log(entry.clone()); let _ = app.emit("vcs:log", entry); - let _ = app.emit("git-progress", ProgressPayload { message: msg }); + let _ = app.emit("vcs-progress", ProgressPayload { message: msg }); }) } diff --git a/Backend/src/tauri_commands/stash.rs b/Backend/src/tauri_commands/stash.rs index 366db6b..28aad08 100644 --- a/Backend/src/tauri_commands/stash.rs +++ b/Backend/src/tauri_commands/stash.rs @@ -19,22 +19,22 @@ use super::{current_repo_or_err, run_repo_task}; /// # Returns /// - `Ok(Vec)` stash list. /// - `Err(String)` when listing fails. -pub async fn git_stash_list(state: State<'_, AppState>) -> Result, String> { +pub async fn vcs_stash_list(state: State<'_, AppState>) -> Result, String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_stash_list", repo, move |repo| { + run_repo_task("vcs_stash_list", repo, move |repo| { match repo.inner().stash_list() { Ok(items) => { - info!("git_stash_list: count={}", items.len()); + info!("vcs_stash_list: count={}", items.len()); for item in &items { info!( - "git_stash_list: selector='{}' msg='{}' meta='{}'", + "vcs_stash_list: selector='{}' msg='{}' meta='{}'", item.selector, item.msg, item.meta ); } Ok(items) } Err(e) => { - error!("git_stash_list: failed: {}", e); + error!("vcs_stash_list: failed: {}", e); Err(e.to_string()) } } @@ -54,7 +54,7 @@ pub async fn git_stash_list(state: State<'_, AppState>) -> Result /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on stash failure. -pub async fn git_stash_push( +pub async fn vcs_stash_push( state: State<'_, AppState>, message: Option, include_untracked: Option, @@ -68,7 +68,7 @@ pub async fn git_stash_push( .into_iter() .map(PathBuf::from) .collect(); - run_repo_task("git_stash_push", repo, move |repo| { + run_repo_task("vcs_stash_push", repo, move |repo| { repo.inner() .stash_push(&msg, iu, &pathbufs) .map_err(|e| e.to_string()) @@ -86,13 +86,13 @@ pub async fn git_stash_push( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on apply failure. -pub async fn git_stash_apply( +pub async fn vcs_stash_apply( state: State<'_, AppState>, selector: Option, ) -> Result<(), String> { let repo = current_repo_or_err(&state)?; let selector = selector.unwrap_or_default(); - run_repo_task("git_stash_apply", repo, move |repo| { + run_repo_task("vcs_stash_apply", repo, move |repo| { repo.inner() .stash_apply(selector.as_str()) .map_err(|e| e.to_string()) @@ -110,13 +110,13 @@ pub async fn git_stash_apply( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on pop failure. -pub async fn git_stash_pop( +pub async fn vcs_stash_pop( state: State<'_, AppState>, selector: Option, ) -> Result<(), String> { let repo = current_repo_or_err(&state)?; let selector = selector.unwrap_or_default(); - run_repo_task("git_stash_pop", repo, move |repo| { + run_repo_task("vcs_stash_pop", repo, move |repo| { repo.inner() .stash_pop(selector.as_str()) .map_err(|e| e.to_string()) @@ -134,21 +134,21 @@ pub async fn git_stash_pop( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on drop failure. -pub async fn git_stash_drop( +pub async fn vcs_stash_drop( state: State<'_, AppState>, selector: Option, ) -> Result<(), String> { let repo = current_repo_or_err(&state)?; let selector = selector.unwrap_or_default(); - run_repo_task("git_stash_drop", repo, move |repo| { - info!("git_stash_drop: selector='{}'", selector); + run_repo_task("vcs_stash_drop", repo, move |repo| { + info!("vcs_stash_drop: selector='{}'", selector); match repo.inner().stash_drop(selector.as_str()) { Ok(()) => { - info!("git_stash_drop: success selector='{}'", selector); + info!("vcs_stash_drop: success selector='{}'", selector); Ok(()) } Err(e) => { - error!("git_stash_drop: failed selector='{}': {}", selector, e); + error!("vcs_stash_drop: failed selector='{}': {}", selector, e); Err(e.to_string()) } } @@ -166,13 +166,13 @@ pub async fn git_stash_drop( /// # Returns /// - `Ok(Vec)` patch lines. /// - `Err(String)` on lookup failure. -pub async fn git_stash_show( +pub async fn vcs_stash_show( state: State<'_, AppState>, selector: Option, ) -> Result, String> { let repo = current_repo_or_err(&state)?; let selector = selector.unwrap_or_default(); - run_repo_task("git_stash_show", repo, move |repo| { + run_repo_task("vcs_stash_show", repo, move |repo| { repo.inner() .stash_show(selector.as_str()) .map_err(|e| e.to_string()) diff --git a/Backend/src/tauri_commands/status.rs b/Backend/src/tauri_commands/status.rs index 510fee6..2024847 100644 --- a/Backend/src/tauri_commands/status.rs +++ b/Backend/src/tauri_commands/status.rs @@ -19,17 +19,17 @@ use super::{current_repo_or_err, run_repo_task}; /// # Returns /// - `Ok(StatusPayload)` status details. /// - `Err(String)` when status computation fails. -pub async fn git_status(state: State<'_, AppState>) -> Result { +pub async fn vcs_status(state: State<'_, AppState>) -> Result { let repo = current_repo_or_err(&state)?; - run_repo_task("git_status", repo, move |repo| { - info!("git_status: fetching repo status"); + run_repo_task("vcs_status", repo, move |repo| { + info!("vcs_status: fetching repo status"); let payload = repo.inner().status_payload().map_err(|e| { - error!("git_status: failed to compute status: {e}"); + error!("vcs_status: failed to compute status: {e}"); e.to_string() })?; debug!( - "git_status: files={}, ahead={}, behind={}", + "vcs_status: files={}, ahead={}, behind={}", payload.files.len(), payload.ahead, payload.behind @@ -51,13 +51,13 @@ pub async fn git_status(state: State<'_, AppState>) -> Result)` commit list. /// - `Err(String)` on backend failure. -pub async fn git_log( +pub async fn vcs_log( state: State<'_, AppState>, limit: Option, rev: Option, ) -> Result, String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_log", repo, move |repo| { + run_repo_task("vcs_log", repo, move |repo| { let q = LogQuery { rev, path: None, @@ -85,12 +85,12 @@ pub async fn git_log( /// # Returns /// - `Ok(Vec)` diff lines. /// - `Err(String)` on backend failure. -pub async fn git_diff_file( +pub async fn vcs_diff_file( state: State<'_, AppState>, path: String, ) -> Result, String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_diff_file", repo, move |repo| { + run_repo_task("vcs_diff_file", repo, move |repo| { repo.inner() .diff_file(&PathBuf::from(path)) .map_err(|e| e.to_string()) @@ -108,12 +108,12 @@ pub async fn git_diff_file( /// # Returns /// - `Ok(Vec)` diff lines. /// - `Err(String)` on backend failure. -pub async fn git_diff_commit( +pub async fn vcs_diff_commit( state: State<'_, AppState>, id: String, ) -> Result, String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_diff_commit", repo, move |repo| { + run_repo_task("vcs_diff_commit", repo, move |repo| { repo.inner().diff_commit(&id).map_err(|e| e.to_string()) }) .await @@ -129,12 +129,12 @@ pub async fn git_diff_commit( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on backend failure. -pub async fn git_discard_paths( +pub async fn vcs_discard_paths( state: State<'_, AppState>, paths: Vec, ) -> Result<(), String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_discard_paths", repo, move |repo| { + run_repo_task("vcs_discard_paths", repo, move |repo| { let pb: Vec = paths.into_iter().map(PathBuf::from).collect(); repo.inner().discard_paths(&pb).map_err(|e| e.to_string()) }) @@ -151,9 +151,9 @@ pub async fn git_discard_paths( /// # Returns /// - `Ok(())` on success. /// - `Err(String)` on backend failure. -pub async fn git_discard_patch(state: State<'_, AppState>, patch: String) -> Result<(), String> { +pub async fn vcs_discard_patch(state: State<'_, AppState>, patch: String) -> Result<(), String> { let repo = current_repo_or_err(&state)?; - run_repo_task("git_discard_patch", repo, move |repo| { + run_repo_task("vcs_discard_patch", repo, move |repo| { repo.inner() .apply_reverse_patch(&patch) .map_err(|e| e.to_string()) diff --git a/Backend/src/validate.rs b/Backend/src/validate.rs index e49e293..87d3cdd 100644 --- a/Backend/src/validate.rs +++ b/Backend/src/validate.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::sync::LazyLock; -/// Regex pattern for scp-like Git URLs. +/// Regex pattern for scp-like VCS URLs. static SCP_LIKE_RE: LazyLock = LazyLock::new(|| regex::Regex::new(r"^[\w.-]+@[\w.-]+:[\w./-]+\.git$").unwrap()); @@ -37,15 +37,15 @@ fn normalize_and_probe(input: &str) -> (String, bool, bool) { (s.clone(), p.exists(), p.is_dir()) } -/// Heuristically checks whether a string looks like a Git URL. +/// Heuristically checks whether a string looks like a VCS URL. /// /// # Parameters /// - `u`: Candidate URL string. /// /// # Returns -/// - `true` when URL matches supported Git URL forms. +/// - `true` when URL matches supported VCS URL forms. /// - `false` otherwise. -fn is_probably_git_url(u: &str) -> bool { +fn is_probably_vcs_url(u: &str) -> bool { let u = u.trim(); if u.is_empty() { return false; @@ -87,15 +87,15 @@ fn looks_like_path(s: &str) -> bool { WIN_ABS_RE.is_match(s) } -/// Validates whether a string looks like a supported Git URL. +/// Validates whether a string looks like a supported VCS URL. /// /// # Parameters /// - `url`: Candidate URL string. /// /// # Returns /// - Validation result with `ok` and optional reason. -pub fn validate_git_url(url: String) -> Validation { - if is_probably_git_url(&url) { +pub fn validate_vcs_url(url: String) -> Validation { + if is_probably_vcs_url(&url) { Validation { ok: true, reason: None, @@ -104,7 +104,7 @@ pub fn validate_git_url(url: String) -> Validation { Validation { ok: false, reason: Some( - "Not a recognized Git URL (http(s), ssh, or scp-like ending in .git)".into(), + "Not a recognized VCS URL (http(s), ssh, or scp-like ending in .git)".into(), ), } } @@ -138,12 +138,12 @@ pub fn validate_add_path(path: String) -> Validation { }; } - // Optional: require .git folder present + // Optional: require repository marker present let is_repo = Path::new(&norm).join(".git").exists(); if !is_repo { return Validation { ok: false, - reason: Some("Folder does not look like a Git repository (.git missing)".into()), + reason: Some("Folder does not look like a repository (.git missing)".into()), }; } @@ -162,10 +162,10 @@ pub fn validate_add_path(path: String) -> Validation { /// # Returns /// - Validation result with `ok` and optional reason. pub fn validate_clone_input(url: String, dest: String) -> Validation { - if !is_probably_git_url(&url) { + if !is_probably_vcs_url(&url) { return Validation { ok: false, - reason: Some("Invalid Git URL".into()), + reason: Some("Invalid VCS URL".into()), }; } if !looks_like_path(&dest) { @@ -200,7 +200,7 @@ pub fn validate_clone_input(url: String, dest: String) -> Validation { if Path::new(&norm).join(".git").exists() { return Validation { ok: false, - reason: Some("Destination already contains a Git repo".into()), + reason: Some("Destination already contains a repository".into()), }; } Validation { diff --git a/Frontend/src/scripts/features/branches.ts b/Frontend/src/scripts/features/branches.ts index 3658914..7fef0f4 100644 --- a/Frontend/src/scripts/features/branches.ts +++ b/Frontend/src/scripts/features/branches.ts @@ -41,10 +41,10 @@ function syncBranchLabelsFromState() { async function loadBranches() { try { - const branches = await TAURI.invoke('git_list_branches'); + const branches = await TAURI.invoke('vcs_list_branches'); state.branches = Array.isArray(branches) ? branches : []; - const head = await TAURI.invoke<{ detached: boolean; branch?: string; commit?: string }>('git_head_status'); + const head = await TAURI.invoke<{ detached: boolean; branch?: string; commit?: string }>('vcs_head_status'); if (head?.branch) state.branch = head.branch; const short = (head?.commit || '').slice(0, 7); const label = head?.detached ? `Detached HEAD ${short ? '(' + short + ')' : ''}` : (state.branch || '—'); @@ -173,7 +173,7 @@ export function bindBranchUI() { return; } try { - await TAURI.invoke('git_checkout_branch', { name }); + await TAURI.invoke('vcs_checkout_branch', { name }); await runHook('onSwitchBranch', hookData); await loadBranches(); // resync from backend instead of manual toggles if (options.closePopover) closeBranchPopover(); @@ -213,7 +213,7 @@ export function bindBranchUI() { const ok = await confirmBool(`Merge '${name}' into '${cur}'?`); if (!ok) return; try { - await TAURI.invoke('git_merge_branch', { name }); + await TAURI.invoke('vcs_merge_branch', { name }); notify(`Merged branch '${name}' into '${cur}'`); await Promise.allSettled([renderList(), loadBranches()]); } catch (e) { @@ -262,7 +262,7 @@ export function bindBranchUI() { notify(pre.reason || 'Delete cancelled'); return; } - await TAURI.invoke('git_delete_branch', { name, force: wantForce }); + await TAURI.invoke('vcs_delete_branch', { name, force: wantForce }); await runHook('onBranchDelete', hookData); notify(`${wantForce ? 'Force-deleted' : 'Deleted'} '${name}'`); await loadBranches(); @@ -285,7 +285,7 @@ export function bindBranchUI() { notify(pre.reason || 'Delete cancelled'); return; } - await TAURI.invoke('git_delete_branch', { name, force: true }); + await TAURI.invoke('vcs_delete_branch', { name, force: true }); await runHook('onBranchDelete', hookData); notify(`Force-deleted '${name}'`); await loadBranches(); diff --git a/Frontend/src/scripts/features/cherryPick.ts b/Frontend/src/scripts/features/cherryPick.ts index 2ccd012..7afe7f9 100644 --- a/Frontend/src/scripts/features/cherryPick.ts +++ b/Frontend/src/scripts/features/cherryPick.ts @@ -31,7 +31,7 @@ export function wireCherryPick() { const branch = (branchEl?.value || '').trim(); if (!commit || !branch) return; try { - await TAURI.invoke('git_cherry_pick_to_branch', { id: commit, branch }); + await TAURI.invoke('vcs_cherry_pick_to_branch', { id: commit, branch }); notify(`Cherry-picked onto ${branch}`); closeModal('cherry-pick-modal'); await Promise.allSettled([hydrateBranches(), hydrateStatus(), hydrateCommits()]); diff --git a/Frontend/src/scripts/features/conflicts.ts b/Frontend/src/scripts/features/conflicts.ts index b5a445d..1cc7575 100644 --- a/Frontend/src/scripts/features/conflicts.ts +++ b/Frontend/src/scripts/features/conflicts.ts @@ -30,7 +30,7 @@ async function ensureMergeModal() { const content = textarea?.value ?? ''; const path = currentConflict.path; try { - await TAURI.invoke('git_save_merge_result', { path, content }); + await TAURI.invoke('vcs_save_merge_result', { path, content }); notify('Saved merge result'); closeModal('merge-modal'); await Promise.allSettled([hydrateStatus()]); @@ -87,7 +87,7 @@ async function ensureSummaryModal() { const ok = await confirmBool('Abort the merge? This will discard merge progress.'); if (!ok) return; try { - await TAURI.invoke('git_merge_abort'); + await TAURI.invoke('vcs_merge_abort'); notify('Merge aborted'); closeModal('conflicts-summary-modal'); await hydrateStatus(); @@ -98,7 +98,7 @@ async function ensureSummaryModal() { contBtn?.addEventListener('click', async () => { try { - await TAURI.invoke('git_merge_continue'); + await TAURI.invoke('vcs_merge_continue'); notify('Merge committed'); closeModal('conflicts-summary-modal'); await hydrateStatus(); @@ -124,7 +124,7 @@ export async function openConflictsSummary(files: FileStatus[]): Promise { const conflicted = (Array.isArray(files) ? files : []) .filter((f) => String(f?.status || '').toUpperCase() === 'U' && !!f?.path); - const ctx = await TAURI.invoke<{ in_progress: boolean }>('git_merge_context').catch(() => ({ in_progress: false })); + const ctx = await TAURI.invoke<{ in_progress: boolean }>('vcs_merge_context').catch(() => ({ in_progress: false })); const inMerge = !!ctx?.in_progress; if (subEl) subEl.textContent = inMerge ? 'Resolve conflicts before committing the merge' : 'Resolve conflicts in your working tree'; @@ -167,7 +167,7 @@ export async function openConflictsSummary(files: FileStatus[]): Promise { resolveBtn.textContent = 'Resolve…'; resolveBtn.addEventListener('click', async () => { try { - const details = await TAURI.invoke('git_conflict_details', { path: f.path }); + const details = await TAURI.invoke('vcs_conflict_details', { path: f.path }); await openMergeModal(f, details); } catch (e) { notify(`Failed to open conflict: ${String(e || '')}`); @@ -249,7 +249,7 @@ export async function launchExternalMergeTool(path: string): Promise { return; } try { - await TAURI.invoke('git_launch_merge_tool', { path }); + await TAURI.invoke('vcs_launch_merge_tool', { path }); notify('Opened custom merge tool'); } catch (err) { console.error(err); diff --git a/Frontend/src/scripts/features/diff.ts b/Frontend/src/scripts/features/diff.ts index 7b7c21a..9d4cd54 100644 --- a/Frontend/src/scripts/features/diff.ts +++ b/Frontend/src/scripts/features/diff.ts @@ -44,7 +44,7 @@ export function bindCommit() { let combinedPatch = ''; for (const path of partialFiles) { let lines: string[] = []; - try { lines = await TAURI.invoke('git_diff_file', { path }); } catch {} + try { lines = await TAURI.invoke('vcs_diff_file', { path }); } catch {} if (!Array.isArray(lines) || lines.length === 0) continue; const selHunks = hunksMap[path] || []; const selLines = linesMap[path] || {}; diff --git a/Frontend/src/scripts/features/newBranch.ts b/Frontend/src/scripts/features/newBranch.ts index 116dcd7..6b45512 100644 --- a/Frontend/src/scripts/features/newBranch.ts +++ b/Frontend/src/scripts/features/newBranch.ts @@ -128,7 +128,7 @@ export function wireNewBranch() { return; } } - await TAURI.invoke('git_create_branch', { name, from, checkout }); + await TAURI.invoke('vcs_create_branch', { name, from, checkout }); await runHook('onBranchCreate', hookData); if (checkout) { await runHook('onSwitchBranch', { from: state.branch, to: name }); diff --git a/Frontend/src/scripts/features/renameBranch.ts b/Frontend/src/scripts/features/renameBranch.ts index af9b19d..d7f5ec4 100644 --- a/Frontend/src/scripts/features/renameBranch.ts +++ b/Frontend/src/scripts/features/renameBranch.ts @@ -31,7 +31,7 @@ export function wireRenameBranch() { const newName = (nameEl?.value || '').trim(); if (!oldName || !newName || oldName === newName) return; try { - await TAURI.invoke('git_rename_branch', { old_name: oldName, new_name: newName }); + await TAURI.invoke('vcs_rename_branch', { old_name: oldName, new_name: newName }); notify(`Renamed '${oldName}' → '${newName}'`); // Ask the rest of the app to refresh branch UI window.dispatchEvent(new CustomEvent('app:repo-selected')); diff --git a/Frontend/src/scripts/features/repo/diffView.ts b/Frontend/src/scripts/features/repo/diffView.ts index 11fe942..c22a3af 100644 --- a/Frontend/src/scripts/features/repo/diffView.ts +++ b/Frontend/src/scripts/features/repo/diffView.ts @@ -98,7 +98,7 @@ export async function selectFile(file: FileStatus, index: number) { try { let lines: string[] = []; if (file.path) { - lines = await TAURI.invoke('git_diff_file', { path: file.path }); + lines = await TAURI.invoke('vcs_diff_file', { path: file.path }); } if (status === '?' && file.path && (!Array.isArray(lines) || lines.length === 0)) { try { @@ -147,7 +147,7 @@ export async function selectFile(file: FileStatus, index: number) { try { const patch = buildPatchForSelectedHunks(file.path, state.currentDiff, [hi]); if (patch) { - await TAURI.invoke('git_discard_patch', { patch }); + await TAURI.invoke('vcs_discard_patch', { patch }); await Promise.allSettled([hydrateStatus()]); } } catch (e) { console.error('Discard failed:', e); notify('Discard failed'); } @@ -160,7 +160,7 @@ export async function selectFile(file: FileStatus, index: number) { try { const patch = buildPatchForSelectedHunks(file.path, state.currentDiff, selected); if (patch) { - await TAURI.invoke('git_discard_patch', { patch }); + await TAURI.invoke('vcs_discard_patch', { patch }); await Promise.allSettled([hydrateStatus()]); } } catch (e) { console.error('Discard failed:', e); notify('Discard failed'); } @@ -176,12 +176,12 @@ export async function selectFile(file: FileStatus, index: number) { let patch = ''; for (const p of filesWithSel) { let lines: string[] = []; - try { lines = await TAURI.invoke('git_diff_file', { path: p }); } catch {} + try { lines = await TAURI.invoke('vcs_diff_file', { path: p }); } catch {} if (!Array.isArray(lines) || lines.length === 0) continue; patch += buildPatchForSelectedHunks(p, lines, hunksMap[p]) + '\n'; } if (patch.trim()) { - await TAURI.invoke('git_discard_patch', { patch }); + await TAURI.invoke('vcs_discard_patch', { patch }); await Promise.allSettled([hydrateStatus()]); } } catch (e) { console.error('Discard failed:', e); notify('Discard failed'); } @@ -248,13 +248,13 @@ export async function selectStashDiff(selector: string) { try { let lines: string[] = []; if (selector) { - lines = await TAURI.invoke('git_stash_show', { selector }); + lines = await TAURI.invoke('vcs_stash_show', { selector }); } state.currentDiff = lines || []; diffEl.innerHTML = renderHunksReadonly(state.currentDiff); scrollDiffToTop(); } catch (e) { - console.warn('git_stash_show failed', e); + console.warn('vcs_stash_show failed', e); diffEl.innerHTML = '
Failed to load stash diff
'; scrollDiffToTop(); } @@ -271,7 +271,7 @@ export async function renderCombinedDiff(paths: string[]) { let html = ''; for (const p of files) { try { - const lines = await TAURI.invoke('git_diff_file', { path: p }); + const lines = await TAURI.invoke('vcs_diff_file', { path: p }); html += `
${escapeHtml(p)}
`; const fileLines = Array.isArray(lines) ? lines : []; if (detectBinaryDiff(fileLines)) { @@ -316,7 +316,7 @@ async function renderConflictView(file: FileStatus) { diffEl.innerHTML = '
Loading conflict…
'; scrollDiffToTop(); try { - const details = await TAURI.invoke('git_conflict_details', { path: file.path }); + const details = await TAURI.invoke('vcs_conflict_details', { path: file.path }); diffEl.innerHTML = renderConflictMarkup(details); bindConflictActions(diffEl, file, details); scrollDiffToTop(); @@ -380,7 +380,7 @@ function bindConflictActions(root: HTMLElement, file: FileStatus, details: Confl buttons.forEach((b) => { b.disabled = true; }); container.setAttribute('data-busy', '1'); try { - await TAURI.invoke('git_resolve_conflict_side', { path: file.path, side }); + await TAURI.invoke('vcs_resolve_conflict_side', { path: file.path, side }); notify(side === 'ours' ? 'Kept your version' : 'Kept their version'); await Promise.allSettled([hydrateStatus()]); } catch (err) { diff --git a/Frontend/src/scripts/features/repo/history.ts b/Frontend/src/scripts/features/repo/history.ts index 0d3f095..8560ea1 100644 --- a/Frontend/src/scripts/features/repo/history.ts +++ b/Frontend/src/scripts/features/repo/history.ts @@ -70,7 +70,7 @@ async function openCommitActionsMenu(commit: any, x: number, y: number, opts?: C const ok = await confirmBool(`Revert commit ${short}? This will create a new commit that undoes its changes.`); if (!ok) return; try { - await TAURI.invoke('git_revert_commit', { id: commit.id }); + await TAURI.invoke('vcs_revert_commit', { id: commit.id }); notify('Revert complete'); await Promise.allSettled([hydrateStatus(), hydrateCommits()]); } catch (e) { @@ -86,7 +86,7 @@ async function openCommitActionsMenu(commit: any, x: number, y: number, opts?: C items.push({ label: 'Undo to this commit', action: async () => { try { - await TAURI.invoke('git_undo_to_commit', { id: commit.id }); + await TAURI.invoke('vcs_undo_to_commit', { id: commit.id }); await Promise.allSettled([hydrateStatus(), hydrateCommits()]); } catch (e) { console.error('Undo failed:', e); notify('Undo failed'); } }, @@ -216,7 +216,7 @@ export async function selectHistory(commit: any, index: number) { try { let lines: string[] = []; if (commit.id) { - lines = await TAURI.invoke('git_diff_commit', { id: commit.id }); + lines = await TAURI.invoke('vcs_diff_commit', { id: commit.id }); } const files = parseCommitDiffByFile(lines || []); if (files.length === 0) { @@ -305,7 +305,7 @@ export async function selectHistory(commit: any, index: number) { if (patch && !patch.endsWith('\n')) patch += '\n'; try { - await TAURI.invoke('git_discard_patch', { patch }); + await TAURI.invoke('vcs_discard_patch', { patch }); notify('Reverted file changes (review in Changes tab)'); await Promise.allSettled([hydrateStatus()]); } catch (e) { @@ -320,7 +320,7 @@ export async function selectHistory(commit: any, index: number) { }); } } catch (e) { - console.warn('git_diff_commit failed', e); + console.warn('vcs_diff_commit failed', e); diffEl.innerHTML += '
Failed to load diff
'; } } diff --git a/Frontend/src/scripts/features/repo/hydrate.ts b/Frontend/src/scripts/features/repo/hydrate.ts index 74b042a..1e71937 100644 --- a/Frontend/src/scripts/features/repo/hydrate.ts +++ b/Frontend/src/scripts/features/repo/hydrate.ts @@ -69,8 +69,8 @@ export async function hydrateBranches(): Promise { if (!isTauriRuntimeAvailable()) return false; try { await yieldToPaint(); - const list = await TAURI.invoke('git_list_branches'); - const head = await TAURI.invoke<{ detached: boolean; branch?: string; commit?: string }>('git_head_status').catch(() => ({ detached: false } as any)); + const list = await TAURI.invoke('vcs_list_branches'); + const head = await TAURI.invoke<{ detached: boolean; branch?: string; commit?: string }>('vcs_head_status').catch(() => ({ detached: false } as any)); const has = Array.isArray(list) && list.length > 0; state.hasRepo = state.hasRepo || has; if (has) { @@ -94,13 +94,13 @@ export async function hydrateBranches(): Promise { export async function hydrateStatus() { try { await yieldToPaint(); - const result = await TAURI.invoke<{ files: any[]; ahead?: number; behind?: number }>('git_status'); + const result = await TAURI.invoke<{ files: any[]; ahead?: number; behind?: number }>('vcs_status'); const nextFiles = Array.isArray(result?.files) ? (result.files as any) : []; let nextMergeInProgress = false; let nextSeenConflicts = new Set(); // Track merge context for UI hints (e.g., resolved-conflict checkmarks) try { - const ctx = await TAURI.invoke<{ in_progress: boolean }>('git_merge_context'); + const ctx = await TAURI.invoke<{ in_progress: boolean }>('vcs_merge_context'); nextMergeInProgress = !!ctx?.in_progress; if (nextMergeInProgress) { nextSeenConflicts = new Set(); @@ -158,10 +158,13 @@ export async function hydrateStatus() { } } +/** + * Loads the full commit history so history counts reflect all visible commits. + */ export async function hydrateCommits(): Promise { try { await yieldToPaint(); - const list = await TAURI.invoke('git_log', { limit: 100 }); + const list = await TAURI.invoke('vcs_log', { limit: 0 }); state.hasRepo = true; const baseCommits = Array.isArray(list) ? (list as any) : []; const behindCount = Number((state as any).behind || 0); @@ -177,7 +180,7 @@ export async function hydrateCommits(): Promise { } for (const { range, ref } of ranges) { try { - const remoteList = await TAURI.invoke('git_log', { limit, rev: range }); + const remoteList = await TAURI.invoke('vcs_log', { limit, rev: range }); if (Array.isArray(remoteList) && remoteList.length > 0) { incoming = remoteList.map((c: any) => ({ ...c, incoming: true, remoteRef: ref })); break; @@ -200,7 +203,7 @@ export async function hydrateCommits(): Promise { const aheadCount = Number((state as any).ahead || 0); if (aheadCount > 0) { try { - const aheadList = await TAURI.invoke('git_log', { limit: 1000, rev: '@{upstream}..HEAD' }); + const aheadList = await TAURI.invoke('vcs_log', { limit: 1000, rev: '@{upstream}..HEAD' }); const ids = new Set(); (aheadList || []).forEach((c: any) => { if (c?.id) ids.add(String(c.id)); }); (state as any).aheadIds = ids; @@ -220,7 +223,7 @@ export async function hydrateCommits(): Promise { export async function hydrateStash(): Promise { try { await yieldToPaint(); - const list = await TAURI.invoke('git_stash_list'); + const list = await TAURI.invoke('vcs_stash_list'); (state as any).stash = Array.isArray(list) ? (list as any) : []; if (prefs.tab === 'stash') renderList(); } catch (e) { diff --git a/Frontend/src/scripts/features/repo/interactions.ts b/Frontend/src/scripts/features/repo/interactions.ts index 831142d..2f6f6c5 100644 --- a/Frontend/src/scripts/features/repo/interactions.ts +++ b/Frontend/src/scripts/features/repo/interactions.ts @@ -264,7 +264,7 @@ export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { const ok = await confirmBool(`Add ${label} to .gitignore?`); if (!ok) return; try { - await TAURI.invoke('git_add_to_gitignore_paths', { paths: targets }); + await TAURI.invoke('vcs_add_to_gitignore_paths', { paths: targets }); notify(targets.length > 1 ? 'Added to .gitignore' : 'Added to .gitignore'); await Promise.allSettled([hydrateStatus()]); } catch { @@ -277,14 +277,14 @@ export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { const paths = selectedPaths.slice(); const ok = await confirmBool(`Discard all changes in ${paths.length} selected file(s)? This cannot be undone.`); if (!ok) return; - try { await TAURI.invoke('git_discard_paths', { paths }); await Promise.allSettled([hydrateStatus()]); } + try { await TAURI.invoke('vcs_discard_paths', { paths }); await Promise.allSettled([hydrateStatus()]); } catch (e) { console.error('Discard failed:', e); notify('Discard failed'); } }}); } items.push({ label: 'Discard changes', action: async () => { const ok = await confirmBool(`Discard all changes in \n${f.path}? This cannot be undone.`); if (!ok) return; - try { await TAURI.invoke('git_discard_paths', { paths: [f.path] }); await Promise.allSettled([hydrateStatus()]); } + try { await TAURI.invoke('vcs_discard_paths', { paths: [f.path] }); await Promise.allSettled([hydrateStatus()]); } catch (e) { console.error('Discard failed:', e); notify('Discard failed'); } }}); diff --git a/Frontend/src/scripts/features/repo/stash.ts b/Frontend/src/scripts/features/repo/stash.ts index dad6d25..6bc9f12 100644 --- a/Frontend/src/scripts/features/repo/stash.ts +++ b/Frontend/src/scripts/features/repo/stash.ts @@ -77,7 +77,7 @@ export function renderStashList(query: string): boolean { const items: CtxItem[] = []; items.push({ label: 'Apply stash', action: async () => { try { - await TAURI.invoke('git_stash_apply', { selector: target }); + await TAURI.invoke('vcs_stash_apply', { selector: target }); notify('Applied stash'); await Promise.allSettled([hydrateStatus(), hydrateStash()]); renderListRef?.(); @@ -87,7 +87,7 @@ export function renderStashList(query: string): boolean { const ok = await confirmBool(`Delete ${target}? This cannot be undone.`); if (!ok) return; try { - await TAURI.invoke('git_stash_drop', { selector: target }); + await TAURI.invoke('vcs_stash_drop', { selector: target }); notify('Deleted stash'); if (state.currentStash === target) state.currentStash = ''; await Promise.allSettled([hydrateStash()]); @@ -116,7 +116,7 @@ export async function selectStash(item: StashListItem, index: number) { const p = document.querySelector('#stash-pop-btn'); if (p) p.disabled = false; const d = document.querySelector('#stash-drop-btn'); if (d) d.disabled = false; } catch (e) { - console.warn('git_stash_show failed', e); + console.warn('vcs_stash_show failed', e); diffEl.innerHTML = '
Failed to load stash diff
'; } } @@ -191,11 +191,11 @@ function wireStashFooterButtons(container: HTMLElement) { const selector = getActiveStashSelector(); if (!selector) return; try { - await TAURI.invoke('git_stash_apply', { selector }); + await TAURI.invoke('vcs_stash_apply', { selector }); notify('Applied stash'); await Promise.allSettled([hydrateStatus(), hydrateStash()]); renderListRef?.(); - } catch (e) { console.error('git_stash_apply failed:', e); notify('Failed to apply stash'); } + } catch (e) { console.error('vcs_stash_apply failed:', e); notify('Failed to apply stash'); } }); const popBtn = container.querySelector('#stash-pop-btn'); @@ -203,11 +203,11 @@ function wireStashFooterButtons(container: HTMLElement) { const selector = getActiveStashSelector(); if (!selector) return; try { - await TAURI.invoke('git_stash_pop', { selector }); + await TAURI.invoke('vcs_stash_pop', { selector }); notify('Popped stash'); await Promise.allSettled([hydrateStatus(), hydrateStash()]); renderListRef?.(); - } catch (e) { console.error('git_stash_pop failed:', e); notify('Failed to pop stash'); } + } catch (e) { console.error('vcs_stash_pop failed:', e); notify('Failed to pop stash'); } }); const dropBtn = container.querySelector('#stash-drop-btn'); @@ -217,11 +217,11 @@ function wireStashFooterButtons(container: HTMLElement) { const ok = await confirmBool(`Drop ${selector}? This cannot be undone.`); if (!ok) return; try { - await TAURI.invoke('git_stash_drop', { selector }); + await TAURI.invoke('vcs_stash_drop', { selector }); notify('Dropped stash'); state.currentStash = ''; await Promise.allSettled([hydrateStash()]); renderListRef?.(); - } catch (e) { console.error('git_stash_drop failed:', e); notify('Failed to drop stash'); } + } catch (e) { console.error('vcs_stash_drop failed:', e); notify('Failed to drop stash'); } }); } diff --git a/Frontend/src/scripts/features/repoSettings.ts b/Frontend/src/scripts/features/repoSettings.ts index 39f0325..39f7ece 100644 --- a/Frontend/src/scripts/features/repoSettings.ts +++ b/Frontend/src/scripts/features/repoSettings.ts @@ -124,7 +124,7 @@ export async function wireRepoSettings() { await TAURI.invoke('set_repo_settings', { cfg: next }); if (remotesChanged) { // Remote-tracking branches only exist after a fetch; do it once after remotes are modified. - try { await TAURI.invoke('git_fetch_all', {}); } catch { /* ignore */ } + try { await TAURI.invoke('vcs_fetch_all', {}); } catch { /* ignore */ } } saveBtn.classList.add('saved-state'); saveBtn.textContent = 'Saved!'; diff --git a/Frontend/src/scripts/features/setUpstream.ts b/Frontend/src/scripts/features/setUpstream.ts index 6b81e3d..4b369b5 100644 --- a/Frontend/src/scripts/features/setUpstream.ts +++ b/Frontend/src/scripts/features/setUpstream.ts @@ -28,7 +28,7 @@ export function wireSetUpstream() { const upstream = (selectEl?.value || "").trim(); if (!branch || !upstream) return; try { - await TAURI.invoke("git_set_upstream", { branch, upstream }); + await TAURI.invoke("vcs_set_upstream", { branch, upstream }); notify(`Tracking '${upstream}'`); closeModal("set-upstream-modal"); await Promise.allSettled([hydrateStatus(), hydrateCommits()]); diff --git a/Frontend/src/scripts/features/sshAuth.ts b/Frontend/src/scripts/features/sshAuth.ts index 58b4ac7..0f18388 100644 --- a/Frontend/src/scripts/features/sshAuth.ts +++ b/Frontend/src/scripts/features/sshAuth.ts @@ -75,7 +75,7 @@ function wireAuthModal() { if (!https) return; httpsBtn.disabled = true; try { - await TAURI.invoke('git_set_remote_url', { name: current.remote, url: https }); + await TAURI.invoke('vcs_set_remote_url', { name: current.remote, url: https }); notify(`Remote '${current.remote}' set to HTTPS`); closeModal('ssh-auth-modal'); } catch (e) { diff --git a/Frontend/src/scripts/features/sshHostkey.ts b/Frontend/src/scripts/features/sshHostkey.ts index 39ae73f..00f0a78 100644 --- a/Frontend/src/scripts/features/sshHostkey.ts +++ b/Frontend/src/scripts/features/sshHostkey.ts @@ -47,7 +47,7 @@ function wireModalOnce() { setBusy(true); try { await TAURI.invoke('ssh_trust_host', { host: current.host }); - await TAURI.invoke('git_fetch_all', {}); + await TAURI.invoke('vcs_fetch_all', {}); await hydrateBranches(); notify(`Trusted ${current.host}`); closeModal('ssh-hostkey-modal'); diff --git a/Frontend/src/scripts/features/stashConfirm.ts b/Frontend/src/scripts/features/stashConfirm.ts index 86adb02..4dfd463 100644 --- a/Frontend/src/scripts/features/stashConfirm.ts +++ b/Frontend/src/scripts/features/stashConfirm.ts @@ -104,14 +104,14 @@ export function wireStashConfirm() { try { const payload: Record = { message, includeUntracked }; if (overridePaths && overridePaths.length) payload.paths = overridePaths; - await TAURI.invoke('git_stash_push', payload); + await TAURI.invoke('vcs_stash_push', payload); notify('Created stash'); closeModal('stash-confirm-modal'); if (typeof onSuccess === 'function') { await onSuccess(message); } } catch (e) { - console.warn('git_stash_push failed', e); + console.warn('vcs_stash_push failed', e); notify('Failed to create stash'); } finally { resetButton(); diff --git a/Frontend/src/scripts/main.ts b/Frontend/src/scripts/main.ts index b5f4108..7306072 100644 --- a/Frontend/src/scripts/main.ts +++ b/Frontend/src/scripts/main.ts @@ -189,7 +189,7 @@ async function boot() { let success = false; try { ctl.setBusy('Fetching…'); - await TAURI.invoke('git_fetch', {}); + await TAURI.invoke('vcs_fetch', {}); notify('Fetched'); if (hydrate) { await yieldToPaint(); @@ -213,7 +213,7 @@ async function boot() { let success = false; try { ctl.setBusy('Fetching all…'); - await TAURI.invoke('git_fetch_all', {}); + await TAURI.invoke('vcs_fetch_all', {}); notify('Fetched all remotes'); if (hydrate) { await yieldToPaint(); @@ -284,7 +284,7 @@ async function boot() { try { ctl.setBusy('Pulling…'); - const res = await TAURI.invoke<{ pulled: boolean; branch: string; reason?: string | null }>('git_pull', {}); + const res = await TAURI.invoke<{ pulled: boolean; branch: string; reason?: string | null }>('vcs_pull', {}); if (res?.pulled) { notify('Pulled latest changes'); } else { @@ -338,7 +338,7 @@ async function boot() { notify(pre.reason || 'Push cancelled'); return; } - setBusy('Pushing…'); await TAURI.invoke('git_push', {}); + setBusy('Pushing…'); await TAURI.invoke('vcs_push', {}); await runHook('onPush', hookData); notify('Pushed'); await Promise.allSettled([hydrateStatus(), hydrateCommits()]); @@ -439,7 +439,7 @@ async function boot() { const clearBusy = () => { if (statusEl) statusEl.classList.remove('busy'); }; try { setBusy('Undoing…'); - await TAURI.invoke('git_undo_since_push', {}); + await TAURI.invoke('vcs_undo_since_push', {}); notify('Undid unpushed commits'); await Promise.allSettled([hydrateStatus(), hydrateCommits()]); } catch (e) { console.error('Undo failed:', e); notify('Undo failed'); } finally { clearBusy(); } @@ -493,7 +493,7 @@ async function boot() { setBusy('Working…', focused); }); }; - TAURI.listen?.('git-progress', ({ payload }) => { + TAURI.listen?.('vcs-progress', ({ payload }) => { // Don't spam the footer with raw git output; keep it generic. void payload; // Avoid spinner-driven repaint churn for passive/background progress. @@ -599,7 +599,7 @@ async function boot() { } headPollInFlight = (async () => { try { - const head = await TAURI.invoke<{ detached: boolean; branch?: string; commit?: string }>('git_head_status'); + const head = await TAURI.invoke<{ detached: boolean; branch?: string; commit?: string }>('vcs_head_status'); const key = `${head?.detached ? 1 : 0}:${String(head?.branch || '')}:${String(head?.commit || '')}`; if (key === lastHeadKey) return; From a479ff292b711531f8b09ffe1a55041377ad0bf7 Mon Sep 17 00:00:00 2001 From: Jordon Date: Mon, 27 Apr 2026 18:05:16 +0100 Subject: [PATCH 02/11] Improved button labelling --- ARCHITECTURE.md | 1 + Backend/src/lib.rs | 1 + Backend/src/plugin_bundles.rs | 83 ++++++++++++++++++- Backend/src/plugin_vcs_backends.rs | 12 ++- Backend/src/tauri_commands/backends.rs | 18 ++++ Backend/src/tauri_commands/general.rs | 2 +- Frontend/src/scripts/features/repo/hydrate.ts | 20 +++++ Frontend/src/scripts/features/repo/index.ts | 2 +- Frontend/src/scripts/main.ts | 28 ++++--- Frontend/src/scripts/state/state.test.ts | 25 ++++++ Frontend/src/scripts/state/state.ts | 14 ++++ Frontend/src/scripts/ui/layout.ts | 13 ++- docs/plugin architecture.md | 6 +- 13 files changed, 198 insertions(+), 27 deletions(-) create mode 100644 Frontend/src/scripts/state/state.test.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0306417..4f81aae 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -44,6 +44,7 @@ Backend: Feature-facing backend API lives under `Backend/src/tauri_commands/`. - Backend/plugin boundary: Backend communicates with plugin processes over JSON-RPC over stdio. +- Repo-open UI labels can be resolved from the active backend via backend-provided action-label maps; generic VCS text remains the fallback. - Settings boundary: Backend persists/loads app configuration and mediates environment application. diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 5efa15e..a027450 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -322,6 +322,7 @@ fn build_invoke_handler( tauri_commands::current_repo_path, tauri_commands::list_recent_repos, tauri_commands::vcs_list_branches, + tauri_commands::current_vcs_action_labels, tauri_commands::vcs_status, tauri_commands::vcs_log, tauri_commands::vcs_stash_list, diff --git a/Backend/src/plugin_bundles.rs b/Backend/src/plugin_bundles.rs index b9cb667..8ba6f60 100644 --- a/Backend/src/plugin_bundles.rs +++ b/Backend/src/plugin_bundles.rs @@ -112,6 +112,8 @@ pub enum VcsBackendProvide { id: String, #[serde(default)] name: Option, + #[serde(default)] + action_labels: BTreeMap, }, } @@ -147,12 +149,23 @@ pub struct PluginBundleStore { root: PathBuf, } +/// Installed VCS backend metadata resolved from a plugin module. +#[derive(Debug, Clone)] +pub struct ModuleVcsBackend { + /// Logical backend identifier. + pub id: String, + /// Optional human-readable backend name. + pub name: Option, + /// Optional action-label map keyed by namespaced VCS actions. + pub action_labels: BTreeMap, +} + /// Installed module component metadata and resolved executable path. #[derive(Debug, Clone)] pub struct ModuleComponent { pub exec: String, pub exec_path: PathBuf, - pub vcs_backends: Vec<(String, Option)>, + pub vcs_backends: Vec, } /// Active component metadata for a plugin selected by `current.json`. @@ -718,9 +731,17 @@ impl PluginBundleStore { .filter_map(|backend| match backend { VcsBackendProvide::Id(id) => { let id = id.trim().to_string(); - (!id.is_empty()).then_some((id, None)) + (!id.is_empty()).then_some(ModuleVcsBackend { + id, + name: None, + action_labels: BTreeMap::new(), + }) } - VcsBackendProvide::Named { id, name } => { + VcsBackendProvide::Named { + id, + name, + action_labels, + } => { let id = id.trim().to_string(); if id.is_empty() { return None; @@ -730,7 +751,11 @@ impl PluginBundleStore { .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string); - Some((id, name)) + Some(ModuleVcsBackend { + id, + name, + action_labels, + }) } }) .collect(), @@ -872,6 +897,19 @@ mod tests { fs::write(root.join("bin").join("plugin.js"), "export {};\n").unwrap(); } + /// Writes a prepared plugin directory with backend action labels. + fn write_plugin_with_labels(root: &Path, plugin_id: &str) { + fs::create_dir_all(root.join("bin")).unwrap(); + fs::write( + root.join("package.json"), + format!( + "{{\n \"name\": \"{plugin_id}\",\n \"version\": \"0.1.0\",\n \"openvcs\": {{\n \"id\": \"{plugin_id}\",\n \"name\": \"Test\",\n \"version\": \"0.1.0\",\n \"module\": {{\n \"exec\": \"plugin.js\",\n \"vcs_backends\": [{{\n \"id\": \"git\",\n \"name\": \"Git\",\n \"action_labels\": {{\n \"VCS.Commit\": \"Commit\",\n \"VCS.Push\": \"Push\"\n }}\n }}]\n }}\n }}\n}}\n" + ), + ) + .unwrap(); + fs::write(root.join("bin").join("plugin.js"), "export {};\n").unwrap(); + } + #[test] fn install_prepared_plugin_dir_writes_index_and_source() { let dir = tempdir().unwrap(); @@ -898,4 +936,41 @@ mod tests { .is_some_and(|metadata| metadata.kind == "path") ); } + + #[test] + fn load_current_components_reads_backend_action_labels() { + let dir = tempdir().unwrap(); + let store = PluginBundleStore::new_at(dir.path().join("plugins")); + let prepared = dir.path().join("prepared"); + write_plugin_with_labels(&prepared, "example.plugin"); + + store + .install_prepared_plugin_dir( + &prepared, + &InstalledPluginSourceMetadata { + managed_by: "user-config".to_string(), + kind: "path".to_string(), + spec: "../example".to_string(), + }, + true, + ) + .unwrap(); + + let components = store.load_current_components("example.plugin").unwrap(); + let module = components.and_then(|c| c.module).expect("module component"); + let backend = module + .vcs_backends + .into_iter() + .find(|backend| backend.id == "git") + .expect("git backend"); + assert_eq!(backend.name.as_deref(), Some("Git")); + assert_eq!( + backend.action_labels.get("VCS.Commit").map(String::as_str), + Some("Commit") + ); + assert_eq!( + backend.action_labels.get("VCS.Push").map(String::as_str), + Some("Push") + ); + } } diff --git a/Backend/src/plugin_vcs_backends.rs b/Backend/src/plugin_vcs_backends.rs index ccb2092..58712fa 100644 --- a/Backend/src/plugin_vcs_backends.rs +++ b/Backend/src/plugin_vcs_backends.rs @@ -11,7 +11,8 @@ use crate::plugin_runtime::settings_store; use crate::plugin_runtime::{vcs_proxy::PluginVcsProxy, PluginRuntimeManager}; use crate::settings::AppConfig; use log::{debug, error, info, trace, warn}; -use std::{collections::BTreeMap, path::Path, sync::Arc}; +use std::collections::BTreeMap; +use std::{path::Path, sync::Arc}; const MODULE: &str = "plugin_vcs_backends"; @@ -48,6 +49,8 @@ pub struct PluginBackendDescriptor { pub backend_id: BackendId, /// Optional human-readable backend name. pub backend_name: Option, + /// Optional action-label map keyed by namespaced VCS actions. + pub action_labels: BTreeMap, /// Owning plugin identifier. pub plugin_id: String, /// Optional human-readable plugin name. @@ -131,15 +134,16 @@ pub fn list_plugin_vcs_backends() -> Result, String continue; }; - for (id, name) in module.vcs_backends { - let backend_id = BackendId::from(id.as_str()); + for backend in module.vcs_backends { + let backend_id = BackendId::from(backend.id.as_str()); debug!( "list_plugin_vcs_backends: found backend '{}' from plugin '{}'", backend_id, p.plugin_id ); let candidate = PluginBackendDescriptor { backend_id: backend_id.clone(), - backend_name: name, + backend_name: backend.name, + action_labels: backend.action_labels, plugin_id: p.plugin_id.clone(), plugin_name: p.name.clone(), }; diff --git a/Backend/src/tauri_commands/backends.rs b/Backend/src/tauri_commands/backends.rs index 559edfe..06c332d 100644 --- a/Backend/src/tauri_commands/backends.rs +++ b/Backend/src/tauri_commands/backends.rs @@ -67,6 +67,24 @@ pub fn list_vcs_backends_cmd(state: State<'_, AppState>) -> Vec<(String, String) backends } +#[tauri::command] +/// Returns the action-label map for the currently selected backend. +/// +/// # Parameters +/// - `state`: Shared application state. +/// +/// # Returns +/// - A list of `(action_key, label)` tuples for the active backend. +pub fn current_vcs_action_labels( + state: State<'_, AppState>, +) -> Result, String> { + let repo = state + .current_repo() + .ok_or_else(|| "No repository selected".to_string())?; + let desc = plugin_vcs_backends::plugin_vcs_backend_descriptor(&repo.id())?; + Ok(desc.action_labels.into_iter().collect()) +} + #[tauri::command] /// Sets the default backend and reopens the current repository with it when possible. /// diff --git a/Backend/src/tauri_commands/general.rs b/Backend/src/tauri_commands/general.rs index 6c236ff..3d9259f 100644 --- a/Backend/src/tauri_commands/general.rs +++ b/Backend/src/tauri_commands/general.rs @@ -64,7 +64,7 @@ pub async fn browse_directory( ) -> Option { let title = match purpose.as_deref() { Some("clone_dest") => "Choose destination folder", - Some("add_repo") => "Select an existing Git repository folder", + Some("add_repo") => "Select an existing repository folder", _ => "Select a folder", }; utilities::browse_directory_async(window.app_handle().clone(), title).await diff --git a/Frontend/src/scripts/features/repo/hydrate.ts b/Frontend/src/scripts/features/repo/hydrate.ts index 1e71937..1f68370 100644 --- a/Frontend/src/scripts/features/repo/hydrate.ts +++ b/Frontend/src/scripts/features/repo/hydrate.ts @@ -231,3 +231,23 @@ export async function hydrateStash(): Promise { (state as any).stash = []; } } + +/** + * Loads the resolved action-label map for the active backend. + */ +export async function hydrateVcsActionLabels(): Promise { + try { + const labels = await TAURI.invoke>('current_vcs_action_labels'); + const resolved: Record = {}; + for (const pair of labels || []) { + if (!Array.isArray(pair) || pair.length < 2) continue; + const key = String(pair[0] || '').trim(); + const label = String(pair[1] || '').trim(); + if (!key || !label) continue; + resolved[key] = label; + } + state.vcsActionLabels = resolved; + } catch { + state.vcsActionLabels = {}; + } +} diff --git a/Frontend/src/scripts/features/repo/index.ts b/Frontend/src/scripts/features/repo/index.ts index a65a07f..6aa15dd 100644 --- a/Frontend/src/scripts/features/repo/index.ts +++ b/Frontend/src/scripts/features/repo/index.ts @@ -3,4 +3,4 @@ export { bindRepoHotkeys } from './hotkeys'; export { bindFilter } from './filter'; export { renderList, wireRenderListCallbacks } from './list'; -export { hydrateBranches, hydrateStatus, hydrateCommits, hydrateStash, yieldToPaint } from './hydrate'; +export { hydrateBranches, hydrateStatus, hydrateCommits, hydrateStash, hydrateVcsActionLabels, yieldToPaint } from './hydrate'; diff --git a/Frontend/src/scripts/main.ts b/Frontend/src/scripts/main.ts index 7306072..42a2c61 100644 --- a/Frontend/src/scripts/main.ts +++ b/Frontend/src/scripts/main.ts @@ -8,7 +8,7 @@ import { qs } from './lib/dom'; import { notify } from './lib/notify'; import { setStatus } from './lib/status'; import { destroyOverlayScrollbarsFor, initOverlayScrollbarsFor, refreshOverlayScrollbarsFor } from './lib/scrollbars'; -import { prefs, state, hasRepo } from './state/state'; +import { prefs, state, hasRepo, resolveVcsActionLabel } from './state/state'; import { bindTabs, initResizer, refreshRepoActions, setRepoHeader, resetRepoHeader, setTab, setTheme, bindLayoutActionState @@ -16,7 +16,7 @@ import { import { clearPluginMenubarMenus, initMenubar, refreshPluginMenubarMenus } from './ui/menubar'; import { closeAllModals } from './ui/modals'; import { bindCommandSheet, openSheet, closeSheet } from './features/commandSheet'; -import { bindRepoHotkeys, bindFilter, renderList, wireRenderListCallbacks, hydrateBranches, hydrateStatus, hydrateCommits, hydrateStash, yieldToPaint } from './features/repo'; +import { bindRepoHotkeys, bindFilter, renderList, wireRenderListCallbacks, hydrateBranches, hydrateStatus, hydrateCommits, hydrateStash, hydrateVcsActionLabels, yieldToPaint } from './features/repo'; import { bindBranchUI } from './features/branches'; import { bindCommit } from './features/diff'; import { openAbout } from './features/about'; @@ -188,7 +188,7 @@ async function boot() { const ctl = status ?? statusController(); let success = false; try { - ctl.setBusy('Fetching…'); + ctl.setBusy(`${resolveVcsActionLabel('VCS.Fetch', 'Fetch')}…`); await TAURI.invoke('vcs_fetch', {}); notify('Fetched'); if (hydrate) { @@ -243,10 +243,12 @@ async function boot() { const behind = getBehindCount(); const repoOn = hasRepo(); const canPull = repoOn; - const mainLabel = behind > 0 ? `Pull (${behind})` : 'Fetch'; + const fetchLabel = resolveVcsActionLabel('VCS.Fetch', 'Fetch'); + const pullLabel = resolveVcsActionLabel('VCS.Pull', 'Pull'); + const mainLabel = behind > 0 ? `${pullLabel} (${behind})` : fetchLabel; const mainTitle = behind > 0 - ? `Pull ${behind} commit${behind === 1 ? '' : 's'} (F5)` - : 'Fetch (F5)'; + ? `${pullLabel} ${behind} commit${behind === 1 ? '' : 's'} (F5)` + : `${fetchLabel} (F5)`; if (fetchBtn) { fetchBtn.textContent = mainLabel; @@ -262,18 +264,18 @@ async function boot() { fetchOnlyItem.setAttribute('aria-disabled', 'false'); fetchOnlyItem.tabIndex = 0; const name = fetchOnlyItem.querySelector('.name'); - if (name) name.textContent = 'Fetch'; + if (name) name.textContent = fetchLabel; } if (fetchAllItem) { fetchAllItem.setAttribute('aria-disabled', 'false'); fetchAllItem.tabIndex = 0; } if (pullItem) { - const pullLabel = behind > 0 ? `Pull (${behind})` : 'Pull'; + const pullText = behind > 0 ? `${pullLabel} (${behind})` : pullLabel; pullItem.setAttribute('aria-disabled', canPull ? 'false' : 'true'); pullItem.tabIndex = canPull ? 0 : -1; const name = pullItem.querySelector('.name'); - if (name) name.textContent = pullLabel; + if (name) name.textContent = pullText; } } @@ -283,7 +285,7 @@ async function boot() { if (!fetched) { ctl.clearBusy(); return; } try { - ctl.setBusy('Pulling…'); + ctl.setBusy(`${resolveVcsActionLabel('VCS.Pull', 'Pull')}ing…`); const res = await TAURI.invoke<{ pulled: boolean; branch: string; reason?: string | null }>('vcs_pull', {}); if (res?.pulled) { notify('Pulled latest changes'); @@ -296,7 +298,7 @@ async function boot() { ctl.clearBusy(); } - await Promise.allSettled([hydrateBranches(), hydrateStatus(), hydrateCommits(), hydrateStash()]); + await Promise.allSettled([hydrateBranches(), hydrateStatus(), hydrateCommits(), hydrateStash(), hydrateVcsActionLabels()]); } async function defaultFetchAction() { @@ -513,7 +515,7 @@ async function boot() { await hydrateBranches(); setRepoHeader(path); - await Promise.allSettled([hydrateStatus(), hydrateCommits()]); + await Promise.allSettled([hydrateStatus(), hydrateCommits(), hydrateVcsActionLabels()]); updateFetchUI(); // Broadcast app-level event so branch UI and actions can sync @@ -569,7 +571,7 @@ async function boot() { if (doFetch) { await fetchCurrentRemoteOnly({ hydrate: false }); } - await Promise.allSettled([hydrateBranches(), hydrateStatus(), hydrateCommits(), hydrateStash()]); + await Promise.allSettled([hydrateBranches(), hydrateStatus(), hydrateCommits(), hydrateStash(), hydrateVcsActionLabels()]); updateFetchUI(); })(); try { diff --git a/Frontend/src/scripts/state/state.test.ts b/Frontend/src/scripts/state/state.test.ts new file mode 100644 index 0000000..fb4b39e --- /dev/null +++ b/Frontend/src/scripts/state/state.test.ts @@ -0,0 +1,25 @@ +// Copyright © 2025-2026 OpenVCS Contributors +// SPDX-License-Identifier: GPL-3.0-or-later +import { describe, expect, it } from 'vitest'; + +/** Provides a minimal `matchMedia` test shim used by state imports. */ +function createMatchMediaMock(query: string) { + return { matches: false, media: query, addListener: () => {}, removeListener: () => {} }; +} + +// Set matchMedia before importing modules that touch browser media APIs. +(globalThis as any).matchMedia = createMatchMediaMock; + +import { resolveVcsActionLabel, state } from './state'; + +describe('resolveVcsActionLabel', () => { + it('falls back to generic VCS text when a label is missing', () => { + state.vcsActionLabels = {}; + expect(resolveVcsActionLabel('VCS.Push', 'Push')).toBe('Push'); + }); + + it('returns the plugin-provided label when available', () => { + state.vcsActionLabels = { 'VCS.Push': 'Ship' }; + expect(resolveVcsActionLabel('VCS.Push', 'Push')).toBe('Ship'); + }); +}); diff --git a/Frontend/src/scripts/state/state.ts b/Frontend/src/scripts/state/state.ts index 88d9fa6..399b13b 100644 --- a/Frontend/src/scripts/state/state.ts +++ b/Frontend/src/scripts/state/state.ts @@ -45,6 +45,7 @@ export const state = { branches: [] as Branch[], // list of branches files: [] as FileStatus[], // working tree status commits: [] as CommitItem[], // recent commits + vcsActionLabels: {} as Record, // resolved action labels from the active backend selectedCommit: null as CommitItem | null, stash: [] as StashItem[], // stash entries ahead: 0 as number, // commits ahead of upstream @@ -78,6 +79,19 @@ export const hasRepo = (): boolean => Boolean(state.hasRepo); export const hasChanges = (): boolean => Array.isArray(state.files) && state.files.length > 0; +/** + * Resolves a backend-provided VCS action label with a generic fallback. + * @param actionKey - Stable namespaced action key such as `VCS.Push`. + * @param fallback - Generic text to use when no label is available. + * @returns The resolved user-facing label. + */ +export function resolveVcsActionLabel(actionKey: string, fallback: string): string { + const key = String(actionKey || '').trim(); + if (!key) return fallback; + const label = state.vcsActionLabels[key]; + return String(label || '').trim() || fallback; +} + /** * Get display label for a file status code. * @param s - Status code character diff --git a/Frontend/src/scripts/ui/layout.ts b/Frontend/src/scripts/ui/layout.ts index ea47421..d67f9a4 100644 --- a/Frontend/src/scripts/ui/layout.ts +++ b/Frontend/src/scripts/ui/layout.ts @@ -1,7 +1,7 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later import { qs, qsa, setText } from '../lib/dom'; -import { prefs, savePrefs, state, hasRepo, hasChanges } from '../state/state'; +import { prefs, savePrefs, state, hasRepo, hasChanges, resolveVcsActionLabel } from '../state/state'; import { TAURI } from '../lib/tauri'; import { notify } from '../lib/notify'; import { setAppearanceMode } from '../themes'; @@ -202,9 +202,10 @@ export function refreshRepoActions() { if (pushBtn) { pushBtn.classList.toggle('attention', repoOn && ahead > 0); const labelEl = pushBtn.querySelector('.btn-label'); + const base = resolveVcsActionLabel('VCS.Push', 'Push'); const label = repoOn && ahead > 0 - ? `Push (${ahead})` - : 'Push'; + ? `${base} (${ahead})` + : base; pushBtn.title = label; pushBtn.setAttribute('aria-label', label); if (labelEl) labelEl.textContent = label; @@ -223,6 +224,12 @@ export function refreshRepoActions() { .some((k) => !!(state as any).selectedLinesByFile[k] && Object.keys((state as any).selectedLinesByFile[k] || {}).length > 0); const filesSelected = !!((state as any).selectedFiles && (state as any).selectedFiles.size > 0); if (commit) commit.disabled = !(repoOn && changesOn && summaryFilled && (hunksSelected || linesSelected || filesSelected)); + if (commit) { + const commitLabel = resolveVcsActionLabel('VCS.Commit', 'Commit'); + commit.textContent = commitLabel; + commit.title = commitLabel; + commit.setAttribute('aria-label', commitLabel); + } // Left-panel undo visibility (under files list) const showUndo = repoOn && ahead > 0 && prefs.tab === 'changes'; diff --git a/docs/plugin architecture.md b/docs/plugin architecture.md index ed1fa2d..8421889 100644 --- a/docs/plugin architecture.md +++ b/docs/plugin architecture.md @@ -144,7 +144,11 @@ The host currently consumes these manifest fields from `package.json.openvcs`: - `name`, `version` (optional but recommended) - `default_enabled` (optional) - `module.exec` (optional Node entry filename under `bin/`) -- `module.vcs_backends` (optional VCS backend ids the module provides) +- `module.vcs_backends` (optional VCS backend ids or backend objects the module provides) + +Backend objects may include a namespaced action-label map such as `VCS.Push` +→ `Push`, `VCS.Pull` → `Pull`, and `VCS.Commit` → `Commit`. The client falls +back to generic VCS text when a label is missing. `module.exec` must resolve to a `.js`, `.mjs`, or `.cjs` file inside `bin/`. From 31c52b942c6e92e043b1d8094608c06c3bac3b69 Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 28 Apr 2026 01:25:43 +0100 Subject: [PATCH 03/11] Update opencode-review.yml --- .github/workflows/opencode-review.yml | 92 +++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 43bfa41..8945ccb 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -8,9 +8,9 @@ jobs: review: if: github.event.pull_request.user.login == 'Jordonbc' runs-on: ubuntu-latest + permissions: - id-token: write - contents: write + contents: read pull-requests: write issues: write @@ -31,12 +31,44 @@ jobs: model: ${{ vars.OPENCODE_REVIEW_MODEL }} 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. + Before doing anything else, read and follow AGENTS.md for this repository and for any affected module. + + You are reviewing this pull request only. Do not edit files, do not commit changes, and do not attempt to rewrite the PR. + + Review only the changes introduced by this pull request, using surrounding repository context only when needed to understand correctness. + + Focus on: + - Correctness bugs + - Regressions + - Unsafe assumptions + - Error handling problems + - API misuse + - Race conditions, lifetime issues, ownership issues, or resource leaks + - Test coverage gaps where the changed behaviour is not adequately covered + - Maintainability issues that would realistically matter in this codebase + + Do not comment on: + - Pure style preferences unless AGENTS.md or existing repository conventions clearly require them + - Hypothetical rewrites + - Broad architecture advice unrelated to this PR + - Trivial naming or formatting issues unless they obscure correctness + - Missing tests for code that has no meaningful behavioural change + + For every finding: + - Cite the specific file and changed line/range when possible + - Explain why it is a real issue + - Explain the likely impact + - Suggest the smallest reasonable fix + - State confidence as High, Medium, or Low + + Use this severity scale: + - Blocking: correctness, data loss, security, build failure, test failure, serious regression + - Important: likely bug, maintainability risk, missing validation, meaningful test gap + - Minor: small cleanup with clear value + + Prefer fewer, higher-confidence comments over many speculative comments. + + If there are no substantive issues, say that no blocking or important issues were found. Do not invent issues to appear useful. - name: fallback if: ${{ steps.review_primary.outcome == 'failure' }} @@ -49,9 +81,41 @@ jobs: model: ${{ vars.OPENCODE_REVIEW_MODEL_FALLBACK }} 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. + Before doing anything else, read and follow AGENTS.md for this repository and for any affected module. + + You are reviewing this pull request only. Do not edit files, do not commit changes, and do not attempt to rewrite the PR. + + Review only the changes introduced by this pull request, using surrounding repository context only when needed to understand correctness. + + Focus on: + - Correctness bugs + - Regressions + - Unsafe assumptions + - Error handling problems + - API misuse + - Race conditions, lifetime issues, ownership issues, or resource leaks + - Test coverage gaps where the changed behaviour is not adequately covered + - Maintainability issues that would realistically matter in this codebase + + Do not comment on: + - Pure style preferences unless AGENTS.md or existing repository conventions clearly require them + - Hypothetical rewrites + - Broad architecture advice unrelated to this PR + - Trivial naming or formatting issues unless they obscure correctness + - Missing tests for code that has no meaningful behavioural change + + For every finding: + - Cite the specific file and changed line/range when possible + - Explain why it is a real issue + - Explain the likely impact + - Suggest the smallest reasonable fix + - State confidence as High, Medium, or Low + + Use this severity scale: + - Blocking: correctness, data loss, security, build failure, test failure, serious regression + - Important: likely bug, maintainability risk, missing validation, meaningful test gap + - Minor: small cleanup with clear value + + Prefer fewer, higher-confidence comments over many speculative comments. + + If there are no substantive issues, say that no blocking or important issues were found. Do not invent issues to appear useful. From 336d1bfa1f703f713898cd5cb6c41df7cd90f50f Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 28 Apr 2026 01:30:34 +0100 Subject: [PATCH 04/11] Update opencode-review.yml --- .github/workflows/opencode-review.yml | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 8945ccb..1ac235d 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -68,6 +68,25 @@ jobs: Prefer fewer, higher-confidence comments over many speculative comments. + Pay special attention to semantic mismatches: + - Comments that do not match the code behaviour + - Variable names that imply different behaviour than the implementation + - Frontend/backend/plugin contract mismatches + - Changed values passed across process, IPC, API, or plugin boundaries + - Sentinel values such as 0, -1, null, undefined, empty strings, and empty arrays + - Type-safe code that may still be behaviourally wrong + + When reviewing sentinel values: + - Do not assume 0 means "unlimited", "default", "disabled", or "empty" unless the implementation explicitly proves it + - Trace the value through every visible layer before deciding it is safe + - If the final meaning depends on plugin/backend behaviour not visible in the PR, flag it as a Medium-confidence issue or explicit question + - Treat contradictions between comments and runtime behaviour as review-worthy + + Before concluding that there are no substantive issues: + - Re-check any finding where the code behaviour appears to contradict a comment, variable name, or stated intent + - Do not treat "the code compiles" as evidence that behaviour is correct + - If you are relying on an assumption about downstream behaviour, say so explicitly + If there are no substantive issues, say that no blocking or important issues were found. Do not invent issues to appear useful. - name: fallback @@ -118,4 +137,23 @@ jobs: Prefer fewer, higher-confidence comments over many speculative comments. + Pay special attention to semantic mismatches: + - Comments that do not match the code behaviour + - Variable names that imply different behaviour than the implementation + - Frontend/backend/plugin contract mismatches + - Changed values passed across process, IPC, API, or plugin boundaries + - Sentinel values such as 0, -1, null, undefined, empty strings, and empty arrays + - Type-safe code that may still be behaviourally wrong + + When reviewing sentinel values: + - Do not assume 0 means "unlimited", "default", "disabled", or "empty" unless the implementation explicitly proves it + - Trace the value through every visible layer before deciding it is safe + - If the final meaning depends on plugin/backend behaviour not visible in the PR, flag it as a Medium-confidence issue or explicit question + - Treat contradictions between comments and runtime behaviour as review-worthy + + Before concluding that there are no substantive issues: + - Re-check any finding where the code behaviour appears to contradict a comment, variable name, or stated intent + - Do not treat "the code compiles" as evidence that behaviour is correct + - If you are relying on an assumption about downstream behaviour, say so explicitly + If there are no substantive issues, say that no blocking or important issues were found. Do not invent issues to appear useful. From 6832798e548a26f873c5807b7d23d72d69b60d0d Mon Sep 17 00:00:00 2001 From: Jordon Date: Tue, 28 Apr 2026 01:33:36 +0100 Subject: [PATCH 05/11] Update opencode-review.yml --- .github/workflows/opencode-review.yml | 42 ++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 1ac235d..52ceba2 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -63,7 +63,7 @@ jobs: Use this severity scale: - Blocking: correctness, data loss, security, build failure, test failure, serious regression - - Important: likely bug, maintainability risk, missing validation, meaningful test gap + - Important: likely bug, maintainability risk, missing validation, meaningful test gap, or unproven cross-layer behaviour - Minor: small cleanup with clear value Prefer fewer, higher-confidence comments over many speculative comments. @@ -71,6 +71,7 @@ jobs: Pay special attention to semantic mismatches: - Comments that do not match the code behaviour - Variable names that imply different behaviour than the implementation + - Function names that imply different behaviour than the implementation - Frontend/backend/plugin contract mismatches - Changed values passed across process, IPC, API, or plugin boundaries - Sentinel values such as 0, -1, null, undefined, empty strings, and empty arrays @@ -78,14 +79,24 @@ jobs: When reviewing sentinel values: - Do not assume 0 means "unlimited", "default", "disabled", or "empty" unless the implementation explicitly proves it - - Trace the value through every visible layer before deciding it is safe - - If the final meaning depends on plugin/backend behaviour not visible in the PR, flag it as a Medium-confidence issue or explicit question + - Do not assume null, undefined, empty strings, or empty arrays preserve existing/default behaviour unless the implementation explicitly proves it + - Trace the changed value through every visible layer before deciding it is safe + - If a changed value crosses frontend/backend/plugin boundaries and its final meaning is not explicitly proven, flag it as an Important finding + - If behaviour depends on a downstream plugin, backend, external command, or runtime convention that is not visible in the PR, flag the assumption instead of treating it as safe - Treat contradictions between comments and runtime behaviour as review-worthy + Treat contradictory intent as a real bug: + - If a comment, variable name, function name, or PR intent says the code should do one thing, but the implementation appears to do another, flag it as an Important finding + - Do not rationalise contradictions as intentional unless the implementation explicitly proves that interpretation + - Do not describe contradictory behaviour as correct + - Example: if code claims to load "full history" but passes `limit: 0`, and the visible backend preserves `0` as `0`, you must flag that as a potential bug unless another visible layer explicitly maps `0` to unlimited + Before concluding that there are no substantive issues: - - Re-check any finding where the code behaviour appears to contradict a comment, variable name, or stated intent + - Re-check any finding where the code behaviour appears to contradict a comment, variable name, function name, or stated intent + - Re-check every changed sentinel value that crosses a frontend/backend/plugin boundary - Do not treat "the code compiles" as evidence that behaviour is correct - - If you are relying on an assumption about downstream behaviour, say so explicitly + - Do not infer intended behaviour from comments alone + - If you are relying on an assumption about downstream behaviour, say so explicitly and treat it as a review concern If there are no substantive issues, say that no blocking or important issues were found. Do not invent issues to appear useful. @@ -132,7 +143,7 @@ jobs: Use this severity scale: - Blocking: correctness, data loss, security, build failure, test failure, serious regression - - Important: likely bug, maintainability risk, missing validation, meaningful test gap + - Important: likely bug, maintainability risk, missing validation, meaningful test gap, or unproven cross-layer behaviour - Minor: small cleanup with clear value Prefer fewer, higher-confidence comments over many speculative comments. @@ -140,6 +151,7 @@ jobs: Pay special attention to semantic mismatches: - Comments that do not match the code behaviour - Variable names that imply different behaviour than the implementation + - Function names that imply different behaviour than the implementation - Frontend/backend/plugin contract mismatches - Changed values passed across process, IPC, API, or plugin boundaries - Sentinel values such as 0, -1, null, undefined, empty strings, and empty arrays @@ -147,13 +159,23 @@ jobs: When reviewing sentinel values: - Do not assume 0 means "unlimited", "default", "disabled", or "empty" unless the implementation explicitly proves it - - Trace the value through every visible layer before deciding it is safe - - If the final meaning depends on plugin/backend behaviour not visible in the PR, flag it as a Medium-confidence issue or explicit question + - Do not assume null, undefined, empty strings, or empty arrays preserve existing/default behaviour unless the implementation explicitly proves it + - Trace the changed value through every visible layer before deciding it is safe + - If a changed value crosses frontend/backend/plugin boundaries and its final meaning is not explicitly proven, flag it as an Important finding + - If behaviour depends on a downstream plugin, backend, external command, or runtime convention that is not visible in the PR, flag the assumption instead of treating it as safe - Treat contradictions between comments and runtime behaviour as review-worthy + Treat contradictory intent as a real bug: + - If a comment, variable name, function name, or PR intent says the code should do one thing, but the implementation appears to do another, flag it as an Important finding + - Do not rationalise contradictions as intentional unless the implementation explicitly proves that interpretation + - Do not describe contradictory behaviour as correct + - Example: if code claims to load "full history" but passes `limit: 0`, and the visible backend preserves `0` as `0`, you must flag that as a potential bug unless another visible layer explicitly maps `0` to unlimited + Before concluding that there are no substantive issues: - - Re-check any finding where the code behaviour appears to contradict a comment, variable name, or stated intent + - Re-check any finding where the code behaviour appears to contradict a comment, variable name, function name, or stated intent + - Re-check every changed sentinel value that crosses a frontend/backend/plugin boundary - Do not treat "the code compiles" as evidence that behaviour is correct - - If you are relying on an assumption about downstream behaviour, say so explicitly + - Do not infer intended behaviour from comments alone + - If you are relying on an assumption about downstream behaviour, say so explicitly and treat it as a review concern If there are no substantive issues, say that no blocking or important issues were found. Do not invent issues to appear useful. From b6dcd98b66ad5fd3e3985d3844d840ffaf0a6fa9 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 29 Apr 2026 01:59:59 +0100 Subject: [PATCH 06/11] Update status.rs --- Backend/src/tauri_commands/status.rs | 37 +++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Backend/src/tauri_commands/status.rs b/Backend/src/tauri_commands/status.rs index 2024847..b712aaf 100644 --- a/Backend/src/tauri_commands/status.rs +++ b/Backend/src/tauri_commands/status.rs @@ -10,6 +10,18 @@ use crate::state::AppState; use super::{current_repo_or_err, run_repo_task}; +/// Normalizes the commit history limit for `vcs_log`. +/// +/// A missing limit keeps the historical default of 100 commits, `0` means +/// unlimited, and positive limits are clamped to the backend safety cap. +/// The frontend uses `0` when it wants the full history. +fn normalize_log_limit(limit: Option) -> u32 { + match limit.unwrap_or(100) { + 0 => 0, + n => n.min(1000) as u32, + } +} + #[tauri::command] /// Returns repository status payload (files + ahead/behind). /// @@ -65,7 +77,7 @@ pub async fn vcs_log( until_utc: None, author_contains: None, skip: 0, - limit: (limit.unwrap_or(100)).min(1000) as u32, + limit: normalize_log_limit(limit), topo_order: true, include_merges: true, }; @@ -75,6 +87,29 @@ pub async fn vcs_log( .await } +#[cfg(test)] +mod tests { + use super::normalize_log_limit; + + #[test] + /// Verifies the default history limit remains 100 commits. + fn normalize_log_limit_defaults_to_100() { + assert_eq!(normalize_log_limit(None), 100); + } + + #[test] + /// Verifies a zero limit requests the full history. + fn normalize_log_limit_treats_zero_as_unlimited() { + assert_eq!(normalize_log_limit(Some(0)), 0); + } + + #[test] + /// Verifies large limits are clamped to the backend cap. + fn normalize_log_limit_clamps_large_values() { + assert_eq!(normalize_log_limit(Some(2_000)), 1_000); + } +} + #[tauri::command] /// Returns diff lines for a single file. /// From bfcf13a89b78302fed011b95a09b7315758dd8e4 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 29 Apr 2026 02:00:08 +0100 Subject: [PATCH 07/11] Update hydrate.ts --- Frontend/src/scripts/features/repo/hydrate.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Frontend/src/scripts/features/repo/hydrate.ts b/Frontend/src/scripts/features/repo/hydrate.ts index 1f68370..82690c5 100644 --- a/Frontend/src/scripts/features/repo/hydrate.ts +++ b/Frontend/src/scripts/features/repo/hydrate.ts @@ -159,7 +159,9 @@ export async function hydrateStatus() { } /** - * Loads the full commit history so history counts reflect all visible commits. + * Loads commit history for the history pane. + * + * Passing `limit: 0` asks the backend for the full history. */ export async function hydrateCommits(): Promise { try { From 1aace4f59d13e47e5fa1cd2e5d86b24d35441be6 Mon Sep 17 00:00:00 2001 From: Jordon Date: Wed, 29 Apr 2026 02:22:56 +0100 Subject: [PATCH 08/11] Improve limit --- Backend/src/core/models.rs | 9 +++++---- Backend/src/tauri_commands/branches.rs | 2 +- Backend/src/tauri_commands/status.rs | 17 ++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Backend/src/core/models.rs b/Backend/src/core/models.rs index e3b2f12..583bbab 100644 --- a/Backend/src/core/models.rs +++ b/Backend/src/core/models.rs @@ -137,8 +137,9 @@ pub struct LogQuery { pub author_contains: Option, /// Number of commits to skip. pub skip: u32, - /// Maximum number of commits to return. - pub limit: u32, + /// Maximum number of commits to return, or `None` for unlimited. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, /// Sort in topological order. pub topo_order: bool, /// Include merge commits. @@ -149,7 +150,7 @@ impl LogQuery { /// Creates a query for the HEAD commit with the given limit. pub fn head(limit: u32) -> Self { Self { - limit, + limit: Some(limit), ..Default::default() } } @@ -216,7 +217,7 @@ mod tests { /// Verifies `LogQuery::head` sets only the limit field. fn log_query_head_sets_limit_and_defaults_rest() { let query = LogQuery::head(25); - assert_eq!(query.limit, 25); + assert_eq!(query.limit, Some(25)); assert!(query.rev.is_none()); assert!(query.path.is_none()); assert_eq!(query.skip, 0); diff --git a/Backend/src/tauri_commands/branches.rs b/Backend/src/tauri_commands/branches.rs index 667d418..af12545 100644 --- a/Backend/src/tauri_commands/branches.rs +++ b/Backend/src/tauri_commands/branches.rs @@ -264,7 +264,7 @@ pub async fn vcs_head_status(state: State<'_, AppState>) -> Result) -> u32 { +/// A missing limit keeps the historical default of 100 commits, `0` becomes +/// `None` (unlimited), and positive limits are clamped to the backend safety cap. +fn normalize_log_limit(limit: Option) -> Option { match limit.unwrap_or(100) { - 0 => 0, - n => n.min(1000) as u32, + 0 => None, + n => Some(n.min(1000) as u32), } } @@ -94,19 +93,19 @@ mod tests { #[test] /// Verifies the default history limit remains 100 commits. fn normalize_log_limit_defaults_to_100() { - assert_eq!(normalize_log_limit(None), 100); + assert_eq!(normalize_log_limit(None), Some(100)); } #[test] /// Verifies a zero limit requests the full history. fn normalize_log_limit_treats_zero_as_unlimited() { - assert_eq!(normalize_log_limit(Some(0)), 0); + assert_eq!(normalize_log_limit(Some(0)), None); } #[test] /// Verifies large limits are clamped to the backend cap. fn normalize_log_limit_clamps_large_values() { - assert_eq!(normalize_log_limit(Some(2_000)), 1_000); + assert_eq!(normalize_log_limit(Some(2_000)), Some(1_000)); } } From cfda442d553af8c3255ede7d5b6732ecca441168 Mon Sep 17 00:00:00 2001 From: Jordon Date: Fri, 1 May 2026 01:52:51 +0100 Subject: [PATCH 09/11] Update hydrate.ts --- Frontend/src/scripts/features/repo/hydrate.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Frontend/src/scripts/features/repo/hydrate.ts b/Frontend/src/scripts/features/repo/hydrate.ts index 82690c5..d6d136b 100644 --- a/Frontend/src/scripts/features/repo/hydrate.ts +++ b/Frontend/src/scripts/features/repo/hydrate.ts @@ -235,7 +235,7 @@ export async function hydrateStash(): Promise { } /** - * Loads the resolved action-label map for the active backend. + * Loads the resolved action-label map for the active backend and notifies the UI. */ export async function hydrateVcsActionLabels(): Promise { try { @@ -251,5 +251,7 @@ export async function hydrateVcsActionLabels(): Promise { state.vcsActionLabels = resolved; } catch { state.vcsActionLabels = {}; + } finally { + window.dispatchEvent(new CustomEvent('app:vcs-action-labels-updated')); } } From aa7b9be67339ac50eacf1cb20143676ae1bdc583 Mon Sep 17 00:00:00 2001 From: Jordon Date: Fri, 1 May 2026 01:53:01 +0100 Subject: [PATCH 10/11] Update main.ts --- Frontend/src/scripts/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/Frontend/src/scripts/main.ts b/Frontend/src/scripts/main.ts index 42a2c61..43c1c99 100644 --- a/Frontend/src/scripts/main.ts +++ b/Frontend/src/scripts/main.ts @@ -636,6 +636,7 @@ async function boot() { window.addEventListener('app:status-updated', updateFetchUI); window.addEventListener('app:branches-updated', updateFetchUI); window.addEventListener('app:repo-selected', updateFetchUI); + window.addEventListener('app:vcs-action-labels-updated', updateFetchUI); window.addEventListener('app:repo-will-switch', clearPluginMenubarMenus); // fetch popover interactions From a408711f5f11960ccf4e11c873bf0fe9641c1780 Mon Sep 17 00:00:00 2001 From: Jordon Date: Fri, 1 May 2026 01:53:03 +0100 Subject: [PATCH 11/11] Update layout.ts --- Frontend/src/scripts/ui/layout.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/Frontend/src/scripts/ui/layout.ts b/Frontend/src/scripts/ui/layout.ts index d67f9a4..c5b4fdd 100644 --- a/Frontend/src/scripts/ui/layout.ts +++ b/Frontend/src/scripts/ui/layout.ts @@ -256,6 +256,7 @@ export function bindLayoutActionState() { window.addEventListener('app:repo-selected', refreshRepoActions); window.addEventListener('app:status-updated', () => { refreshRepoActions(); renderAheadBehind(); }); window.addEventListener('app:branches-updated', () => { setRepoHeader(); refreshRepoActions(); renderAheadBehind(); }); + window.addEventListener('app:vcs-action-labels-updated', refreshRepoActions); // Summary typing should re-evaluate the commit button state qs('#commit-summary')?.addEventListener('input', refreshRepoActions);