diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index b08492b..43bfa41 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest permissions: id-token: write - contents: read + contents: write pull-requests: write issues: write @@ -26,11 +26,12 @@ jobs: env: OPENCODE_API_KEY: ${{ secrets.ZEN_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - #GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: model: ${{ vars.OPENCODE_REVIEW_MODEL }} - use_github_token: false + use_github_token: true prompt: | + Before anything else, read and follow AGENTS.md for this repository and module. Review this pull request: - Check for code quality issues - Look for potential bugs @@ -43,11 +44,12 @@ jobs: env: OPENCODE_API_KEY: ${{ secrets.ZEN_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - #GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: model: ${{ vars.OPENCODE_REVIEW_MODEL_FALLBACK }} - use_github_token: false + use_github_token: true prompt: | + Before anything else, read and follow AGENTS.md for this repository and module. Review this pull request: - Check for code quality issues - Look for potential bugs diff --git a/AGENTS.md b/AGENTS.md index 6b6f3e2..288fedf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - `Backend/`: Rust + Tauri backend (`src/`), commands (`src/tauri_commands/`), plugin runtime (`src/plugin_runtime/`), and config-driven plugin sync support (`scripts/`). - `openvcs.plugins.json`: built-in plugin source list used to materialize shipped plugins during client builds. - `Frontend/`: TypeScript + Vite UI code (`src/scripts/`, `src/styles/`, `src/modals/`), with Vitest tests colocated as `*.test.ts` files. +- OpenVCS is desktop-only: there is no web app, no standalone browser mode, and no supported web browser/WebView deployment target. - `docs/`: UX docs, plugin architecture notes, and plugin/theme packaging guides referenced by contributors. - `packaging/flatpak/`: Flatpak manifests and Flatpak-specific build notes. - Supporting files at the repo root include the workspace `Cargo.toml`, `Justfile`, `README.md`, `ARCHITECTURE.md`, `SECURITY.md`, and installer scripts. @@ -47,7 +48,7 @@ ### Development servers - `cargo tauri dev`: run the desktop app in dev mode (`Backend/` directory). -- `npm --prefix Frontend run dev`: run the frontend-only Vite dev server. +- `npm --prefix Frontend run dev`: run the frontend-only Vite dev server for desktop UI development only; it is not a web app/browser deployment. ## Plugin runtime & host expectations diff --git a/Backend/src/state.rs b/Backend/src/state.rs index cfbcce8..325097a 100644 --- a/Backend/src/state.rs +++ b/Backend/src/state.rs @@ -18,44 +18,6 @@ use serde::{Deserialize, Serialize}; /// Default number of recent repositories stored when settings are missing or invalid. pub const MAX_RECENTS: usize = 10; -/// Applies Git SSH-related environment variables from current settings. -/// -/// # Parameters -/// - `cfg`: Current app configuration. -/// -/// # Returns -/// - `()`. -fn apply_git_ssh_env(cfg: &AppConfig) { - // Prefer config-driven runtime env so the VCS backend (in another crate) can read it. - // Keep env var names stable for packaging and troubleshooting. - unsafe { - // Safety: OpenVCS sets these env vars during startup/config updates and treats them as - // process-wide configuration for child processes (e.g. `git`). - std::env::set_var( - "OPENVCS_SSH_MODE", - match cfg.git.ssh_binary { - crate::settings::GitSshBinary::Auto => "auto", - crate::settings::GitSshBinary::Host => "host", - crate::settings::GitSshBinary::Bundled => "bundled", - crate::settings::GitSshBinary::Custom => "custom", - }, - ); - } - if cfg.git.ssh_binary == crate::settings::GitSshBinary::Custom - && !cfg.git.ssh_path.trim().is_empty() - { - unsafe { - // Safety: see comment above. - std::env::set_var("OPENVCS_SSH", cfg.git.ssh_path.trim()); - } - } else { - unsafe { - // Safety: see comment above. - std::env::remove_var("OPENVCS_SSH"); - } - } -} - /// Central application state. /// Keeps track of the currently open repo and MRU recents. /// Backend choice is tied to each repo (via `Repo::id()`), not stored globally. @@ -84,10 +46,9 @@ impl AppState { /// Creates app state by loading persisted settings and recent repositories. /// /// # Returns - /// - A fully initialized [`AppState`] with config, recents, and runtime env applied. + /// - A fully initialized [`AppState`] with config and recent repositories loaded. pub fn new_with_config() -> Self { let cfg = AppConfig::load_or_default(); // reads ~/.config/openvcs/openvcs.conf - apply_git_ssh_env(&cfg); let s = Self { config: RwLock::new(cfg), repo_config: RwLock::new(RepoConfig::default()), @@ -125,7 +86,6 @@ impl AppState { next.migrate(); next.validate(); next.save().map_err(|e| e.to_string())?; - apply_git_ssh_env(&next); crate::monitoring::sync_backend_monitoring(&next); *self.config.write() = next; self.enforce_recents_limit_and_persist(); diff --git a/Frontend/src/scripts/features/about.ts b/Frontend/src/scripts/features/about.ts index 7ac4689..a6c62e7 100644 --- a/Frontend/src/scripts/features/about.ts +++ b/Frontend/src/scripts/features/about.ts @@ -35,7 +35,7 @@ export async function openAbout(): Promise { if (!modal) return; try { - const info = (TAURI.has ? await TAURI.invoke("about_info").catch(() => null) : null) as + const info = (await TAURI.invoke("about_info").catch(() => null)) as | { version?: string; build?: string; diff --git a/Frontend/src/scripts/features/branches.ts b/Frontend/src/scripts/features/branches.ts index 63c4a2e..3658914 100644 --- a/Frontend/src/scripts/features/branches.ts +++ b/Frontend/src/scripts/features/branches.ts @@ -40,7 +40,6 @@ function syncBranchLabelsFromState() { /* ---------------- data load ---------------- */ async function loadBranches() { - if (!TAURI.has) return; try { const branches = await TAURI.invoke('git_list_branches'); state.branches = Array.isArray(branches) ? branches : []; @@ -174,7 +173,7 @@ export function bindBranchUI() { return; } try { - if (TAURI.has) await TAURI.invoke('git_checkout_branch', { name }); + await TAURI.invoke('git_checkout_branch', { name }); await runHook('onSwitchBranch', hookData); await loadBranches(); // resync from backend instead of manual toggles if (options.closePopover) closeBranchPopover(); @@ -214,7 +213,7 @@ export function bindBranchUI() { const ok = await confirmBool(`Merge '${name}' into '${cur}'?`); if (!ok) return; try { - if (TAURI.has) await TAURI.invoke('git_merge_branch', { name }); + await TAURI.invoke('git_merge_branch', { name }); notify(`Merged branch '${name}' into '${cur}'`); await Promise.allSettled([renderList(), loadBranches()]); } catch (e) { @@ -263,7 +262,7 @@ export function bindBranchUI() { notify(pre.reason || 'Delete cancelled'); return; } - if (TAURI.has) await TAURI.invoke('git_delete_branch', { name, force: wantForce }); + await TAURI.invoke('git_delete_branch', { name, force: wantForce }); await runHook('onBranchDelete', hookData); notify(`${wantForce ? 'Force-deleted' : 'Deleted'} '${name}'`); await loadBranches(); @@ -286,7 +285,7 @@ export function bindBranchUI() { notify(pre.reason || 'Delete cancelled'); return; } - if (TAURI.has) await TAURI.invoke('git_delete_branch', { name, force: true }); + await TAURI.invoke('git_delete_branch', { name, force: true }); await runHook('onBranchDelete', hookData); notify(`Force-deleted '${name}'`); await loadBranches(); diff --git a/Frontend/src/scripts/features/cherryPick.ts b/Frontend/src/scripts/features/cherryPick.ts index 9c315de..2ccd012 100644 --- a/Frontend/src/scripts/features/cherryPick.ts +++ b/Frontend/src/scripts/features/cherryPick.ts @@ -31,10 +31,6 @@ export function wireCherryPick() { const branch = (branchEl?.value || '').trim(); if (!commit || !branch) return; try { - if (!TAURI.has) { - notify('Cherry-pick requires the desktop app'); - return; - } await TAURI.invoke('git_cherry_pick_to_branch', { id: commit, branch }); notify(`Cherry-picked onto ${branch}`); closeModal('cherry-pick-modal'); @@ -74,11 +70,6 @@ export async function openCherryPick(commit: CommitLike) { hydrate('cherry-pick-modal'); wireCherryPick(); - if (!TAURI.has) { - notify('Cherry-pick requires the desktop app'); - return; - } - await hydrateBranches(); const branches = (state.branches || []) .filter((b: any) => { diff --git a/Frontend/src/scripts/features/commandSheet.ts b/Frontend/src/scripts/features/commandSheet.ts index 2376beb..0dd97ac 100644 --- a/Frontend/src/scripts/features/commandSheet.ts +++ b/Frontend/src/scripts/features/commandSheet.ts @@ -35,7 +35,6 @@ function setDisabled(id: string, on: boolean) { } async function validateClone() { - if (!TAURI.has) return; const url = cloneUrl?.value.trim(); const dest = clonePath?.value.trim(); try { @@ -48,7 +47,6 @@ async function validateClone() { } async function validateAdd() { - if (!TAURI.has) return; const path = addPath?.value.trim(); try { const res = await TAURI.invoke<{ ok: boolean; reason?: string }>("validate_add_path", { path }); @@ -186,7 +184,6 @@ export function bindCommandSheet() { // Browse buttons el("#browse-clone", root)?.addEventListener("click", async () => { - if (!TAURI.has) return; try { const dir = await TAURI.invoke("browse_directory", { purpose: "clone_dest" }); if (dir && clonePath) { @@ -197,7 +194,6 @@ export function bindCommandSheet() { }); el("#browse-add", root)?.addEventListener("click", async () => { - if (!TAURI.has) return; try { const dir = await TAURI.invoke("browse_directory", { purpose: "add_repo" }); if (dir && addPath) { @@ -213,7 +209,7 @@ export function bindCommandSheet() { const dest = clonePath?.value.trim(); if (!url || !dest) return; try { - if (TAURI.has) await TAURI.invoke("clone_repo", { url, dest }); + await TAURI.invoke("clone_repo", { url, dest }); await refreshRepoSummary(); // ensure state + event notify(`Cloned ${url} → ${dest}`); closeSheet(); @@ -226,7 +222,7 @@ export function bindCommandSheet() { const path = addPath?.value.trim(); if (!path) return; try { - if (TAURI.has) await TAURI.invoke("add_repo", { path }); + await TAURI.invoke("add_repo", { path }); await refreshRepoSummary(); // ensure state + event notify(`Added ${path}`); closeSheet(); diff --git a/Frontend/src/scripts/features/conflicts.ts b/Frontend/src/scripts/features/conflicts.ts index 2316e44..b5a445d 100644 --- a/Frontend/src/scripts/features/conflicts.ts +++ b/Frontend/src/scripts/features/conflicts.ts @@ -25,7 +25,6 @@ async function ensureMergeModal() { const applyBtn = modal.querySelector('#merge-apply'); applyBtn?.addEventListener('click', async () => { - if (!TAURI.has) { notify('Saving merges requires the desktop app.'); return; } if (!currentConflict) { notify('No conflict selected.'); return; } const textarea = modal.querySelector('#merge-result'); const content = textarea?.value ?? ''; @@ -85,7 +84,6 @@ async function ensureSummaryModal() { const contBtn = modal.querySelector('#conflicts-continue'); abortBtn?.addEventListener('click', async () => { - if (!TAURI.has) return; const ok = await confirmBool('Abort the merge? This will discard merge progress.'); if (!ok) return; try { @@ -99,7 +97,6 @@ async function ensureSummaryModal() { }); contBtn?.addEventListener('click', async () => { - if (!TAURI.has) return; try { await TAURI.invoke('git_merge_continue'); notify('Merge committed'); @@ -114,7 +111,6 @@ async function ensureSummaryModal() { } export async function openConflictsSummary(files: FileStatus[]): Promise { - if (!TAURI.has) return; await ensureSummaryModal(); const modal = document.getElementById('conflicts-summary-modal') as HTMLElement | null; if (!modal) return; @@ -207,7 +203,6 @@ export async function openConflictsSummary(files: FileStatus[]): Promise { } export async function autoOpenFirstConflict(files: FileStatus[]): Promise { - if (!TAURI.has) return; if (!Array.isArray(files) || files.length === 0) return; const conflicted = files.find((f) => String(f?.status || '').toUpperCase() === 'U' && !!f?.path); @@ -230,7 +225,7 @@ export async function autoOpenFirstConflict(files: FileStatus[]): Promise } async function ensureExternalMergeConfig() { - if (externalToolState.loaded || !TAURI.has) return; + if (externalToolState.loaded) return; try { const cfg = await TAURI.invoke('get_global_settings'); const tool = cfg?.diff?.external_merge; @@ -244,13 +239,11 @@ async function ensureExternalMergeConfig() { } export async function hasExternalMergeTool(): Promise { - if (!TAURI.has) return false; await ensureExternalMergeConfig(); return externalToolState.enabled; } export async function launchExternalMergeTool(path: string): Promise { - if (!TAURI.has) { notify('Launching merge tools requires the desktop app.'); return; } if (!(await hasExternalMergeTool())) { notify('No custom merge tool configured'); return; diff --git a/Frontend/src/scripts/features/diff.ts b/Frontend/src/scripts/features/diff.ts index d7443b0..7b7c21a 100644 --- a/Frontend/src/scripts/features/diff.ts +++ b/Frontend/src/scripts/features/diff.ts @@ -50,37 +50,36 @@ export function bindCommit() { const selLines = linesMap[path] || {}; combinedPatch += buildPatchForSelected(path, lines, selHunks, selLines) + '\n'; } - if (TAURI.has) { - if (combinedPatch.trim().length > 0 || selectedFiles.length > 0) { - const hookData = { - summary, - description, - branch: state.branch, - files: selectedFiles, - stagedFiles: stagePaths, - partialFiles, - patch: combinedPatch, - }; - const pre = await runHook('preCommit', hookData); - if (pre.cancelled) { - notify(pre.reason || 'Commit cancelled'); - clearBusy('Ready'); - return; - } - summary = String(hookData.summary || '').trim() || summary; - description = String(hookData.description || ''); - await TAURI.invoke('commit_patch_and_files', { - summary, - description, - patch: combinedPatch, - files: selectedFiles, - stagePaths, - }); - await runHook('onCommit', hookData); - } else { - notify('Select files or hunks to commit'); + if (combinedPatch.trim().length > 0 || selectedFiles.length > 0) { + const hookData = { + summary, + description, + branch: state.branch, + files: selectedFiles, + stagedFiles: stagePaths, + partialFiles, + patch: combinedPatch, + }; + const pre = await runHook('preCommit', hookData); + if (pre.cancelled) { + notify(pre.reason || 'Commit cancelled'); + clearBusy('Ready'); return; } + summary = String(hookData.summary || '').trim() || summary; + description = String(hookData.description || ''); + await TAURI.invoke('commit_patch_and_files', { + summary, + description, + patch: combinedPatch, + files: selectedFiles, + stagePaths, + }); + await runHook('onCommit', hookData); + } + else { + notify('Select files or hunks to commit'); + return; } notify(`Committed to ${state.branch}: ${summary}`); if (commitSummary) commitSummary.value = ''; diff --git a/Frontend/src/scripts/features/newBranch.ts b/Frontend/src/scripts/features/newBranch.ts index 8bebe4e..116dcd7 100644 --- a/Frontend/src/scripts/features/newBranch.ts +++ b/Frontend/src/scripts/features/newBranch.ts @@ -128,7 +128,7 @@ export function wireNewBranch() { return; } } - if (TAURI.has) await TAURI.invoke('git_create_branch', { name, from, checkout }); + await TAURI.invoke('git_create_branch', { name, from, checkout }); await runHook('onBranchCreate', hookData); if (checkout) { await runHook('onSwitchBranch', { from: state.branch, to: name }); diff --git a/Frontend/src/scripts/features/outputLog.ts b/Frontend/src/scripts/features/outputLog.ts index fad10af..2b32faa 100644 --- a/Frontend/src/scripts/features/outputLog.ts +++ b/Frontend/src/scripts/features/outputLog.ts @@ -124,7 +124,6 @@ export async function initOutputLogViewIfRequested(): Promise { const startAppPolling = () => { stopAppPolling(); - if (!TAURI.has) return; const poll = async () => { try { const entries = await TAURI.invoke('tail_app_log', { maxLines: 1500 }); @@ -147,18 +146,14 @@ export async function initOutputLogViewIfRequested(): Promise { }); }); - if (TAURI.has) { - try { + try { const entries = await TAURI.invoke('get_output_log'); append('vcs', Array.isArray(entries) ? entries : []); } catch { // ignore } - } clearBtn?.addEventListener('click', async () => { - if (!TAURI.has) return; - const tab = activeTab(); listFor(tab)?.replaceChildren(); try { diff --git a/Frontend/src/scripts/features/renameBranch.ts b/Frontend/src/scripts/features/renameBranch.ts index b86dd0b..af9b19d 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 { - if (TAURI.has) await TAURI.invoke('git_rename_branch', { old_name: oldName, new_name: newName }); + await TAURI.invoke('git_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 407fb9d..11fe942 100644 --- a/Frontend/src/scripts/features/repo/diffView.ts +++ b/Frontend/src/scripts/features/repo/diffView.ts @@ -97,12 +97,12 @@ export async function selectFile(file: FileStatus, index: number) { try { let lines: string[] = []; - if (TAURI.has && file.path) { + if (file.path) { lines = await TAURI.invoke('git_diff_file', { path: file.path }); } if (status === '?' && file.path && (!Array.isArray(lines) || lines.length === 0)) { try { - const text = TAURI.has ? await TAURI.invoke('read_repo_file_text', { path: file.path }) : ''; + const text = await TAURI.invoke('read_repo_file_text', { path: file.path }); lines = buildUntrackedTextPatch(file.path, text || ''); } catch { lines = [ @@ -142,7 +142,6 @@ export async function selectFile(file: FileStatus, index: number) { const x = mev.clientX, y = mev.clientY; const items: CtxItem[] = []; items.push({ label: 'Discard hunk', action: async () => { - if (!TAURI.has) return; const ok = await confirmBool('Discard this hunk? This cannot be undone.'); if (!ok) return; try { @@ -156,7 +155,6 @@ export async function selectFile(file: FileStatus, index: number) { const selected = (state as any).selectedHunksByFile?.[file.path] as number[] | undefined; if (Array.isArray(selected) && selected.length > 0) { items.push({ label: 'Discard selected hunks (this file)', action: async () => { - if (!TAURI.has) return; const ok = await confirmBool(`Discard ${selected.length} selected hunk(s) in this file? This cannot be undone.`); if (!ok) return; try { @@ -172,7 +170,6 @@ export async function selectFile(file: FileStatus, index: number) { const filesWithSel = Object.keys(hunksMap).filter((k) => Array.isArray(hunksMap[k]) && hunksMap[k].length > 0); if (filesWithSel.length > 0) { items.push({ label: 'Discard selected hunks (all files)', action: async () => { - if (!TAURI.has) return; const ok = await confirmBool(`Discard selected hunks across ${filesWithSel.length} file(s)? This cannot be undone.`); if (!ok) return; try { @@ -250,7 +247,7 @@ export async function selectStashDiff(selector: string) { scrollDiffToTop(); try { let lines: string[] = []; - if (TAURI.has && selector) { + if (selector) { lines = await TAURI.invoke('git_stash_show', { selector }); } state.currentDiff = lines || []; @@ -274,7 +271,7 @@ export async function renderCombinedDiff(paths: string[]) { let html = ''; for (const p of files) { try { - const lines = TAURI.has ? await TAURI.invoke('git_diff_file', { path: p }) : []; + const lines = await TAURI.invoke('git_diff_file', { path: p }); html += `
${escapeHtml(p)}
`; const fileLines = Array.isArray(lines) ? lines : []; if (detectBinaryDiff(fileLines)) { @@ -318,11 +315,6 @@ async function renderConflictView(file: FileStatus) { } diffEl.innerHTML = '
Loading conflict…
'; scrollDiffToTop(); - if (!TAURI.has) { - diffEl.innerHTML = '
Conflict details are only available in the desktop app.
'; - scrollDiffToTop(); - return; - } try { const details = await TAURI.invoke('git_conflict_details', { path: file.path }); diffEl.innerHTML = renderConflictMarkup(details); @@ -384,7 +376,6 @@ function bindConflictActions(root: HTMLElement, file: FileStatus, details: Confl if (!container) return; const resolve = async (side: 'ours' | 'theirs') => { - if (!TAURI.has) { notify('Resolving conflicts requires the desktop app.'); return; } const buttons = container.querySelectorAll('[data-conflict-action]'); buttons.forEach((b) => { b.disabled = true; }); container.setAttribute('data-busy', '1'); diff --git a/Frontend/src/scripts/features/repo/history.ts b/Frontend/src/scripts/features/repo/history.ts index c0686a8..0d3f095 100644 --- a/Frontend/src/scripts/features/repo/history.ts +++ b/Frontend/src/scripts/features/repo/history.ts @@ -61,7 +61,7 @@ async function openCommitActionsMenu(commit: any, x: number, y: number, opts?: C } } - if (TAURI.has && commit?.id) { + if (commit?.id) { items.push({ label: '---' }); items.push({ label: 'Cherry-pick to branch…', action: async () => openCherryPick(commit) }); items.push({ @@ -85,7 +85,6 @@ async function openCommitActionsMenu(commit: any, x: number, y: number, opts?: C items.push({ label: '---' }); items.push({ label: 'Undo to this commit', action: async () => { - if (!TAURI.has) return; try { await TAURI.invoke('git_undo_to_commit', { id: commit.id }); await Promise.allSettled([hydrateStatus(), hydrateCommits()]); @@ -216,7 +215,7 @@ export async function selectHistory(commit: any, index: number) { try { let lines: string[] = []; - if (TAURI.has && commit.id) { + if (commit.id) { lines = await TAURI.invoke('git_diff_commit', { id: commit.id }); } const files = parseCommitDiffByFile(lines || []); @@ -291,10 +290,6 @@ export async function selectHistory(commit: any, index: number) { items.push({ label: '---' }); items.push({ label: 'Revert this file', action: async () => { - if (!TAURI.has) { - notify('Revert requires the desktop app'); - return; - } const block = Array.isArray(file?.lines) ? file.lines : []; const isBinary = block.some((l) => /GIT binary patch|Binary files /i.test(String(l || ''))); if (isBinary) { diff --git a/Frontend/src/scripts/features/repo/hydrate.ts b/Frontend/src/scripts/features/repo/hydrate.ts index 7167c8a..74b042a 100644 --- a/Frontend/src/scripts/features/repo/hydrate.ts +++ b/Frontend/src/scripts/features/repo/hydrate.ts @@ -1,6 +1,6 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later -import { TAURI } from '../../lib/tauri'; +import { TAURI, isTauriRuntimeAvailable } from '../../lib/tauri'; import { state, prefs } from '../../state/state'; import { renderList } from './list'; import { autoOpenFirstConflict } from '../conflicts'; @@ -66,7 +66,7 @@ function describeHydrationFailure(operation: string, error: unknown): string { } export async function hydrateBranches(): Promise { - if (!TAURI.has) return false; + if (!isTauriRuntimeAvailable()) return false; try { await yieldToPaint(); const list = await TAURI.invoke('git_list_branches'); @@ -92,7 +92,6 @@ export async function hydrateBranches(): Promise { } export async function hydrateStatus() { - if (!TAURI.has) return; try { await yieldToPaint(); const result = await TAURI.invoke<{ files: any[]; ahead?: number; behind?: number }>('git_status'); @@ -159,8 +158,7 @@ export async function hydrateStatus() { } } -export async function hydrateCommits() { - if (!TAURI.has) return; +export async function hydrateCommits(): Promise { try { await yieldToPaint(); const list = await TAURI.invoke('git_log', { limit: 100 }); @@ -219,8 +217,7 @@ export async function hydrateCommits() { } } -export async function hydrateStash() { - if (!TAURI.has) return; +export async function hydrateStash(): Promise { try { await yieldToPaint(); const list = await TAURI.invoke('git_stash_list'); diff --git a/Frontend/src/scripts/features/repo/interactions.ts b/Frontend/src/scripts/features/repo/interactions.ts index 0bd2567..831142d 100644 --- a/Frontend/src/scripts/features/repo/interactions.ts +++ b/Frontend/src/scripts/features/repo/interactions.ts @@ -204,8 +204,11 @@ export function toggleSelectAll(on: boolean, visible: FileStatus[]) { export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { ev.preventDefault(); const x = ev.clientX, y = ev.clientY; - const selectedPaths = Array.from(state.selectedFiles || []).filter(Boolean); - const clickedInSelection = !!f.path && (state.selectedFiles?.has(f.path) ?? false); + const selectedPaths = Array.from(state.selectedFiles || []) + .map((path) => path.trim()) + .filter(Boolean); + const clickedPath = (f.path || '').trim(); + const clickedInSelection = !!clickedPath && (state.selectedFiles?.has(clickedPath) ?? false); const explicitMultiSelection = clickedInSelection && selectedPaths.length > 1 && @@ -217,11 +220,12 @@ export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { const items: CtxItem[] = []; /** Opens the stash modal pre-filled for the provided paths. */ const openStashForPaths = (paths: string[], defaultMessage: string) => { - if (!paths.length) return; + const normalizedPaths = paths.map((path) => path.trim()).filter(Boolean); + if (!normalizedPaths.length) return; openStashConfirm({ defaultMessage, includeUntracked: false, - paths, + paths: normalizedPaths, onSuccess: async () => { await Promise.allSettled([hydrateStatus(), hydrateStash()]); renderListCallback?.(); @@ -230,11 +234,7 @@ export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { }; items.push({ label: 'Open with default application', action: async () => { - if (!TAURI.has) { - notify('Open is available in the desktop app'); - return; - } - const target = (hasSingleSelection ? selectedPaths[0] : f.path) || ''; + const target = (hasSingleSelection ? selectedPaths[0] : clickedPath) || ''; if (!target) return; try { await TAURI.invoke('open_repo_file', { path: target }); @@ -249,17 +249,15 @@ export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { openStashForPaths(selectedPaths.slice(), 'WIP selection'); }}); } - const singleTarget = hasSingleSelection ? selectedPaths[0] : f.path; - const defaultMsg = `WIP ${singleTarget}`; - items.push({ label: 'Create stash for this file…', action: () => { - openStashForPaths([singleTarget], defaultMsg); - }}); + const singleTarget = (hasSingleSelection ? selectedPaths[0] : clickedPath) || ''; + if (singleTarget) { + const defaultMsg = `WIP ${singleTarget}`; + items.push({ label: 'Create stash for this file…', action: () => { + openStashForPaths([singleTarget], defaultMsg); + }}); + } items.push({ label: '---' }); items.push({ label: 'Add to .gitignore', action: async () => { - if (!TAURI.has) { - notify('Ignore is available in the desktop app'); - return; - } const targets = (explicitMultiSelection ? selectedPaths.slice() : [f.path]).filter(Boolean); if (!targets.length) return; const label = targets.length > 1 ? `${targets.length} files` : targets[0]; @@ -276,7 +274,6 @@ export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { items.push({ label: '---' }); if (explicitMultiSelection) { items.push({ label: 'Discard all selected', action: async () => { - if (!TAURI.has) return; const paths = selectedPaths.slice(); const ok = await confirmBool(`Discard all changes in ${paths.length} selected file(s)? This cannot be undone.`); if (!ok) return; @@ -285,7 +282,6 @@ export async function onFileContextMenu(ev: MouseEvent, f: FileStatus) { }}); } items.push({ label: 'Discard changes', action: async () => { - if (!TAURI.has) return; 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()]); } diff --git a/Frontend/src/scripts/features/repo/stash.ts b/Frontend/src/scripts/features/repo/stash.ts index acc3750..45fe4d5 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 { - if (!TAURI.has) return; + await TAURI.invoke('git_stash_apply', { selector: target }); notify('Applied stash'); await Promise.allSettled([hydrateStatus(), hydrateStash()]); @@ -88,7 +88,7 @@ export function renderStashList(query: string): boolean { const ok = await confirmBool(`Delete ${target}? This cannot be undone.`); if (!ok) return; try { - if (!TAURI.has) return; + await TAURI.invoke('git_stash_drop', { selector: target }); notify('Deleted stash'); if (state.currentStash === target) state.currentStash = ''; @@ -193,7 +193,7 @@ function wireStashFooterButtons(container: HTMLElement) { const selector = getActiveStashSelector(); if (!selector) return; try { - if (!TAURI.has) return; + await TAURI.invoke('git_stash_apply', { selector }); notify('Applied stash'); await Promise.allSettled([hydrateStatus(), hydrateStash()]); @@ -206,7 +206,7 @@ function wireStashFooterButtons(container: HTMLElement) { const selector = getActiveStashSelector(); if (!selector) return; try { - if (!TAURI.has) return; + await TAURI.invoke('git_stash_pop', { selector }); notify('Popped stash'); await Promise.allSettled([hydrateStatus(), hydrateStash()]); @@ -221,7 +221,7 @@ function wireStashFooterButtons(container: HTMLElement) { const ok = await confirmBool(`Drop ${selector}? This cannot be undone.`); if (!ok) return; try { - if (!TAURI.has) return; + await TAURI.invoke('git_stash_drop', { selector }); notify('Dropped stash'); state.currentStash = ''; diff --git a/Frontend/src/scripts/features/repoSelection.ts b/Frontend/src/scripts/features/repoSelection.ts index 1d51dd7..b02f4ad 100644 --- a/Frontend/src/scripts/features/repoSelection.ts +++ b/Frontend/src/scripts/features/repoSelection.ts @@ -8,7 +8,6 @@ import { state } from '../state/state'; type RepoSummary = { path: string; current_branch: string; branches: { name: string }[] }; export async function refreshRepoSummary() { - if (!TAURI.has) return; try { const info = await TAURI.invoke('get_repo_summary'); state.branch = (info as any).current_branch || ''; diff --git a/Frontend/src/scripts/features/repoSettings.ts b/Frontend/src/scripts/features/repoSettings.ts index 9016825..39f0325 100644 --- a/Frontend/src/scripts/features/repoSettings.ts +++ b/Frontend/src/scripts/features/repoSettings.ts @@ -70,26 +70,24 @@ export async function wireRepoSettings() { remotesEl?.replaceChildren(); } - if (TAURI.has) { - try { - const cfg = await TAURI.invoke('get_repo_settings'); - if (nameInput && cfg?.user_name) nameInput.value = cfg.user_name; - if (emailInput && cfg?.user_email) emailInput.value = cfg.user_email; - - clearRemoteRows(); - const remotes = cfg?.remotes?.length - ? cfg.remotes - : (cfg?.origin_url ? [{ name: 'origin', url: cfg.origin_url }] : []); - - for (const r of remotes) addRemoteRow(r); - initialRemotesKey = JSON.stringify( - remotes - .map(r => ({ name: String(r?.name || '').trim(), url: String(r?.url || '').trim() })) - .filter(r => r.name && r.url) - .sort((a, b) => a.name.localeCompare(b.name)) - ); - } catch { /* ignore */ } - } + try { + const cfg = await TAURI.invoke('get_repo_settings'); + if (nameInput && cfg?.user_name) nameInput.value = cfg.user_name; + if (emailInput && cfg?.user_email) emailInput.value = cfg.user_email; + + clearRemoteRows(); + const remotes = cfg?.remotes?.length + ? cfg.remotes + : (cfg?.origin_url ? [{ name: 'origin', url: cfg.origin_url }] : []); + + for (const r of remotes) addRemoteRow(r); + initialRemotesKey = JSON.stringify( + remotes + .map(r => ({ name: String(r?.name || '').trim(), url: String(r?.url || '').trim() })) + .filter(r => r.name && r.url) + .sort((a, b) => a.name.localeCompare(b.name)) + ); + } catch { /* ignore */ } addRemoteBtn?.addEventListener('click', () => addRemoteRow()); @@ -123,8 +121,8 @@ export async function wireRepoSettings() { remotes, }; try { - if (TAURI.has) await TAURI.invoke('set_repo_settings', { cfg: next }); - if (TAURI.has && remotesChanged) { + 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 */ } } diff --git a/Frontend/src/scripts/features/repoSwitchDrawer.ts b/Frontend/src/scripts/features/repoSwitchDrawer.ts index 63877a6..df88367 100644 --- a/Frontend/src/scripts/features/repoSwitchDrawer.ts +++ b/Frontend/src/scripts/features/repoSwitchDrawer.ts @@ -108,7 +108,7 @@ function renderRecents() { async function openRecent(path: string) { try { - if (TAURI.has) await TAURI.invoke('open_repo', { path }); + await TAURI.invoke('open_repo', { path }); await refreshRepoSummary(); notify(`Opened ${path}`); closeSwitchDrawer(); @@ -121,7 +121,7 @@ async function loadRecents() { if (!recentList) return; try { let raw: unknown = []; - if (TAURI.has) raw = await TAURI.invoke('list_recent_repos').catch(() => []); + raw = await TAURI.invoke('list_recent_repos').catch(() => []); allRecents = Array.isArray(raw) ? raw diff --git a/Frontend/src/scripts/features/setUpstream.ts b/Frontend/src/scripts/features/setUpstream.ts index 8d845d7..6b81e3d 100644 --- a/Frontend/src/scripts/features/setUpstream.ts +++ b/Frontend/src/scripts/features/setUpstream.ts @@ -28,10 +28,6 @@ export function wireSetUpstream() { const upstream = (selectEl?.value || "").trim(); if (!branch || !upstream) return; try { - if (!TAURI.has) { - notify("Set upstream requires the desktop app"); - return; - } await TAURI.invoke("git_set_upstream", { branch, upstream }); notify(`Tracking '${upstream}'`); closeModal("set-upstream-modal"); diff --git a/Frontend/src/scripts/features/settings.ts b/Frontend/src/scripts/features/settings.ts index 71295b1..7d9c8c5 100644 --- a/Frontend/src/scripts/features/settings.ts +++ b/Frontend/src/scripts/features/settings.ts @@ -1,6 +1,6 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later -import { TAURI } from '../lib/tauri'; +import { TAURI, isTauriRuntimeAvailable } from '../lib/tauri'; import { syncFrontendMonitoring } from '../lib/monitoring'; import { openModal, closeModal } from '../ui/modals'; import { toKebab } from '../lib/dom'; @@ -247,7 +247,6 @@ async function renderPluginMenus(modal: HTMLElement): Promise { .querySelectorAll('.panel-form[data-plugin-menu="true"]') .forEach((node) => node.remove()); - if (!TAURI.has) return; let menus: PluginMenuPayload[] = []; let pluginSummaries: PluginSummary[] = []; try { @@ -429,10 +428,9 @@ export function openSettings(section?: string){ // Prevent a "double-click to refresh" feel where the user opens the Theme dropdown // before the async settings/theme list has finished loading. - if (TAURI.has) { - modal.setAttribute('aria-busy', 'true'); - const setThemeAuto = modal.querySelector('#set-theme-auto'); - const setThemeSel = modal.querySelector('#set-theme'); + modal.setAttribute('aria-busy', 'true'); + const setThemeAuto = modal.querySelector('#set-theme-auto'); + const setThemeSel = modal.querySelector('#set-theme'); if (setThemeAuto) setThemeAuto.disabled = true; if (setThemeSel) { setThemeSel.disabled = true; @@ -442,7 +440,6 @@ export function openSettings(section?: string){ opt.textContent = 'Loading…'; setThemeSel.appendChild(opt); } - } loadSettingsIntoForm(modal) .catch(console.error) @@ -559,7 +556,7 @@ export function wireSettings() { panels.addEventListener('click', async (e) => { const btn = (e.target as HTMLElement).closest('button[data-plugin-action][data-plugin-id]'); - if (!btn || !TAURI.has) return; + if (!btn) return; const pluginId = btn.dataset.pluginId || ''; const actionId = btn.dataset.pluginAction || ''; if (!pluginId || !actionId) return; @@ -678,7 +675,6 @@ export function wireSettings() { try { const activePanel = modal.querySelector('#settings-panels .panel-form:not(.hidden)'); if (activePanel?.getAttribute('data-plugin-settings') === 'true') { - if (!TAURI.has) return; const pluginId = String(activePanel.dataset.pluginId || '').trim(); if (!pluginId) { notify('Failed to save plugin settings'); @@ -695,10 +691,8 @@ export function wireSettings() { const next = collectSettingsFromForm(modal); - if (TAURI.has) { - await TAURI.invoke('set_global_settings', { cfg: next }); - await syncFrontendMonitoring(next); - } + await TAURI.invoke('set_global_settings', { cfg: next }); + await syncFrontendMonitoring(next); modal.dataset.currentCfg = JSON.stringify(next); @@ -733,7 +727,6 @@ export function wireSettings() { try { const activePanel = modal.querySelector('#settings-panels .panel-form:not(.hidden)'); if (activePanel?.getAttribute('data-plugin-settings') === 'true') { - if (!TAURI.has) return; const pluginId = String(activePanel.dataset.pluginId || '').trim(); const section = String(activePanel.getAttribute('data-panel') || '').trim(); if (!pluginId) { @@ -748,7 +741,6 @@ export function wireSettings() { return; } - if (!TAURI.has) return; const cur = await TAURI.invoke('get_global_settings'); cur.general = { @@ -892,7 +884,10 @@ export async function loadSettingsIntoForm(root?: HTMLElement) { const m = root || (document.getElementById('settings-modal') as HTMLElement | null); if (!m) return; const get = (sel: string) => m.querySelector(sel); - const cfg = TAURI.has ? await TAURI.invoke('get_global_settings') : null; + let cfg: GlobalSettings | null = null; + try { + cfg = await TAURI.invoke('get_global_settings'); + } catch { /* ignore */ } if (!cfg) return; m.dataset.currentCfg = JSON.stringify(cfg); @@ -946,11 +941,9 @@ async function refreshDefaultBackendOptions(modal: HTMLElement, cfg: GlobalSetti const desired = String(cfg.general?.default_backend || '').trim(); let available: Array<[string, string]> = []; - if (TAURI.has) { - try { - available = await TAURI.invoke>('list_vcs_backends_cmd'); - } catch {} - } + try { + available = await TAURI.invoke>('list_vcs_backends_cmd'); + } catch {} const backends = (Array.isArray(available) ? available : []) .map(([id, name]) => [String(id || '').trim(), String(name || '').trim()] as const) @@ -995,7 +988,6 @@ async function loadPluginsIntoForm(modal: HTMLElement, cfg: GlobalSettings) { if (!(syncConfigBtn as any).dataset?.bound) { (syncConfigBtn as any).dataset.bound = '1'; syncConfigBtn.addEventListener('click', async () => { - if (!TAURI.has) return; try { await TAURI.invoke('sync_configured_plugins'); notify('Reloaded plugin config'); @@ -1182,7 +1174,7 @@ async function loadPluginsIntoForm(modal: HTMLElement, cfg: GlobalSettings) { detailEl.classList.add('empty'); detailEl.textContent = 'Select a plugin to view details.'; - if (!TAURI.has) { + if (!isTauriRuntimeAvailable()) { groupLabelEl.textContent = 'Installed (0 of 0 enabled)'; listEl.replaceChildren(); return; @@ -1239,10 +1231,6 @@ async function loadPluginsIntoForm(modal: HTMLElement, cfg: GlobalSettings) { state.query = String(searchEl.value || '').trim(); const syncStartFailures = async (): Promise => { - if (!TAURI.has) { - state.errorToggleById.clear(); - return; - } try { const failed = await TAURI.invoke('list_plugin_start_failures'); state.errorToggleById = new Set( @@ -1519,7 +1507,6 @@ async function loadPluginsIntoForm(modal: HTMLElement, cfg: GlobalSettings) { }; async function reloadPluginSummaries(): Promise { - if (!TAURI.has) return; let list: PluginSummary[] = []; try { list = await TAURI.invoke('list_plugins'); @@ -1541,7 +1528,6 @@ async function loadPluginsIntoForm(modal: HTMLElement, cfg: GlobalSettings) { renderList(); const persistPluginsDisabled = async () => { - if (!TAURI.has) return; try { const cur = await TAURI.invoke('get_global_settings'); let next: GlobalSettings = { ...(cur || {}) }; @@ -1580,7 +1566,6 @@ async function loadPluginsIntoForm(modal: HTMLElement, cfg: GlobalSettings) { }; const persistSinglePluginToggle = async (pluginId: string, enabled: boolean) => { - if (!TAURI.has) return; const idLower = pluginId.trim().toLowerCase(); try { const activeSection = String( @@ -1733,10 +1718,6 @@ async function loadPluginsIntoForm(modal: HTMLElement, cfg: GlobalSettings) { if (!plugin) return; const label = String(plugin.name || plugin.id || 'plugin'); if (!(await confirmBool(`Remove ${label}? This will delete the plugin bundle.`))) return; - if (!TAURI.has) { - notify('Plugin removal is only available in the desktop app.'); - return; - } try { await TAURI.invoke('uninstall_plugin', { pluginId: id }); notify(`Removed ${label}`); diff --git a/Frontend/src/scripts/features/sshAuth.ts b/Frontend/src/scripts/features/sshAuth.ts index bd198bb..58b4ac7 100644 --- a/Frontend/src/scripts/features/sshAuth.ts +++ b/Frontend/src/scripts/features/sshAuth.ts @@ -70,7 +70,7 @@ function wireAuthModal() { openSshKeysModal(); }); httpsBtn?.addEventListener('click', async () => { - if (!TAURI.has || !current) return; + if (!current) return; const https = sshToHttps(current.url); if (!https) return; httpsBtn.disabled = true; @@ -87,7 +87,7 @@ function wireAuthModal() { } export function initSshAuthPrompt() { - if (!TAURI.has || wired) return; + if (wired) return; wired = true; TAURI.listen?.('ui:ssh-auth', (ev: any) => { diff --git a/Frontend/src/scripts/features/sshHostkey.ts b/Frontend/src/scripts/features/sshHostkey.ts index f32c2a2..39ae73f 100644 --- a/Frontend/src/scripts/features/sshHostkey.ts +++ b/Frontend/src/scripts/features/sshHostkey.ts @@ -43,7 +43,7 @@ function wireModalOnce() { }); acceptBtn?.addEventListener('click', async () => { - if (!TAURI.has || !current) return; + if (!current) return; setBusy(true); try { await TAURI.invoke('ssh_trust_host', { host: current.host }); @@ -63,8 +63,6 @@ function wireModalOnce() { } export function initSshHostkeyPrompt() { - if (!TAURI.has) return; - TAURI.listen?.('ui:ssh-hostkey', (ev: any) => { const p = (ev?.payload || {}) as HostKeyPrompt; openModal('ssh-hostkey-modal'); diff --git a/Frontend/src/scripts/features/sshKeys.ts b/Frontend/src/scripts/features/sshKeys.ts index a29428f..4e76420 100644 --- a/Frontend/src/scripts/features/sshKeys.ts +++ b/Frontend/src/scripts/features/sshKeys.ts @@ -53,7 +53,6 @@ export function wireSshKeys() { } async function refresh() { - if (!TAURI.has) return; if (refreshBtn) refreshBtn.disabled = true; if (addBtn) addBtn.disabled = true; try { @@ -95,7 +94,6 @@ export function wireSshKeys() { copyToClipboard(`ssh-add "${selectedPath.replace(/[\\"]/g, (ch) => '\\' + ch)}"`); }); addBtn?.addEventListener('click', async () => { - if (!TAURI.has) return; if (!selectedPath) { notify('Select a key first'); return; } if (addBtn) addBtn.disabled = true; try { diff --git a/Frontend/src/scripts/features/stashConfirm.ts b/Frontend/src/scripts/features/stashConfirm.ts index 4d54cc2..86adb02 100644 --- a/Frontend/src/scripts/features/stashConfirm.ts +++ b/Frontend/src/scripts/features/stashConfirm.ts @@ -102,7 +102,6 @@ export function wireStashConfirm() { confirmBtn.disabled = true; confirmBtn.textContent = 'Stashing…'; try { - if (!TAURI.has) return; const payload: Record = { message, includeUntracked }; if (overridePaths && overridePaths.length) payload.paths = overridePaths; await TAURI.invoke('git_stash_push', payload); diff --git a/Frontend/src/scripts/features/update.ts b/Frontend/src/scripts/features/update.ts index cf99e72..e4f45ed 100644 --- a/Frontend/src/scripts/features/update.ts +++ b/Frontend/src/scripts/features/update.ts @@ -20,7 +20,6 @@ export function wireUpdate() { const installBtn = modal.querySelector('#update-install') as HTMLButtonElement | null; installBtn?.addEventListener('click', async () => { try { - if (!TAURI.has) return; notify('Downloading update…'); await TAURI.invoke('updater_install_now'); notify('Update installed. Restart to apply.'); @@ -33,8 +32,6 @@ export function wireUpdate() { export async function showUpdateDialog(_data: any) { try { - if (!TAURI.has) return; - const status = await TAURI.invoke('get_update_status'); if (!status.available) { diff --git a/Frontend/src/scripts/lib/logger.ts b/Frontend/src/scripts/lib/logger.ts index f8a6d02..82f66da 100644 --- a/Frontend/src/scripts/lib/logger.ts +++ b/Frontend/src/scripts/lib/logger.ts @@ -41,9 +41,7 @@ function sendToBackend( if (options.breadcrumb !== false) { addFrontendLogBreadcrumb(toMonitoringBreadcrumbLevel(level), message); } - if (TAURI.has) { - TAURI.invoke("log_frontend_message", { level, message }).catch(() => {}); - } + TAURI.invoke("log_frontend_message", { level, message }).catch(() => {}); } /** Maps logger levels to the breadcrumb levels sent through monitoring relay payloads. */ diff --git a/Frontend/src/scripts/lib/monitoring.ts b/Frontend/src/scripts/lib/monitoring.ts index 4cab076..09bb9aa 100644 --- a/Frontend/src/scripts/lib/monitoring.ts +++ b/Frontend/src/scripts/lib/monitoring.ts @@ -3,7 +3,7 @@ import type { GlobalSettings } from '../types'; -import { TAURI } from './tauri'; +import { TAURI, isTauriRuntimeAvailable } from './tauri'; /** Represents the console breadcrumb level forwarded with frontend error reports. */ type MonitoringBreadcrumbLevel = 'debug' | 'info' | 'warning' | 'error'; @@ -63,7 +63,7 @@ export function isFrontendMonitoringAllowed(settings: GlobalSettings | null | un export function shouldEnableFrontendMonitoring( settings: GlobalSettings | null | undefined, ): boolean { - return Boolean(TAURI.has && isFrontendMonitoringAllowed(settings)); + return Boolean(isTauriRuntimeAvailable() && isFrontendMonitoringAllowed(settings)); } /** Synchronizes frontend error relay hooks with the latest settings. */ @@ -151,7 +151,7 @@ function uninstallFrontendMonitoring(): void { async function reportFrontendError( report: Omit, ): Promise { - if (!monitoringEnabled || !TAURI.has) { + if (!monitoringEnabled || !isTauriRuntimeAvailable()) { return; } diff --git a/Frontend/src/scripts/lib/tauri.ts b/Frontend/src/scripts/lib/tauri.ts index 7041e1c..195886b 100644 --- a/Frontend/src/scripts/lib/tauri.ts +++ b/Frontend/src/scripts/lib/tauri.ts @@ -23,17 +23,34 @@ declare global { const core: TauriCore | null = typeof window !== "undefined" && window.__TAURI__?.core ? window.__TAURI__.core : null; const tEvent: TauriEvent | null = typeof window !== "undefined" && window.__TAURI__?.event ? window.__TAURI__.event : null; +const TAURI_RUNTIME_ERROR = 'Failed to initialize Tauri runtime.'; + +/** Returns whether the Tauri runtime core is available. */ +export function isTauriRuntimeAvailable(): boolean { + return !!core; +} /** Tauri API wrapper providing invoke and event listening capabilities. */ export const TAURI = { - /** Whether Tauri runtime is available. */ - has: !!core, - /** Invoke a Tauri command. */ + /** Invoke a Tauri command. Returns a rejected promise if runtime unavailable. */ invoke(cmd: string, args?: Json): Promise { - return core ? core.invoke(cmd, args) : Promise.resolve(undefined as unknown as T); + if (!core) { + return Promise.reject(new Error(TAURI_RUNTIME_ERROR)); + } + return core.invoke(cmd, args); }, - /** Listen for Tauri events. */ + /** Listen for Tauri events. Returns a rejected promise if runtime unavailable. */ listen(event: string, cb: Listener): Promise<{ unlisten: Unlisten }> { - return tEvent ? tEvent.listen(event, cb) : Promise.resolve({ unlisten() {} }); + if (!tEvent) { + return Promise.reject(new Error(TAURI_RUNTIME_ERROR)); + } + return tEvent.listen(event, cb); }, }; + +/** Ensures the desktop runtime is available before boot continues. */ +export function assertDesktopRuntime() { + if (!core || !tEvent) { + throw new Error(TAURI_RUNTIME_ERROR); + } +} diff --git a/Frontend/src/scripts/main.ts b/Frontend/src/scripts/main.ts index bdeb796..b5f4108 100644 --- a/Frontend/src/scripts/main.ts +++ b/Frontend/src/scripts/main.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import './lib/logger'; import { syncFrontendMonitoring } from './lib/monitoring'; -import { TAURI } from './lib/tauri'; +import { TAURI, assertDesktopRuntime, isTauriRuntimeAvailable } from './lib/tauri'; import type { GlobalSettings } from './types'; import { qs } from './lib/dom'; import { notify } from './lib/notify'; @@ -99,6 +99,7 @@ function forceCloseTransientUi() { /** Boots the frontend shell, wires handlers, and hydrates initial state. */ async function boot() { + assertDesktopRuntime(); const cfg = await loadInitialGlobalSettings(); await syncFrontendMonitoring(cfg); @@ -181,7 +182,7 @@ async function boot() { } async function fetchCurrentRemoteOnly(options: { hydrate?: boolean; status?: ReturnType; keepBusy?: boolean } = {}) { - if (!TAURI.has) return false; + if (!isTauriRuntimeAvailable()) return false; return runFetch(async () => { const { hydrate = true, status, keepBusy = false } = options; const ctl = status ?? statusController(); @@ -205,7 +206,7 @@ async function boot() { } async function fetchAllRemotesOnly(options: { hydrate?: boolean; status?: ReturnType; keepBusy?: boolean } = {}) { - if (!TAURI.has) return false; + if (!isTauriRuntimeAvailable()) return false; return runFetch(async () => { const { hydrate = true, status, keepBusy = false } = options; const ctl = status ?? statusController(); @@ -277,7 +278,6 @@ async function boot() { } async function fetchAndPull() { - if (!TAURI.has) return; const ctl = statusController(); const fetched = await fetchCurrentRemoteOnly({ hydrate: false, status: ctl, keepBusy: true }); if (!fetched) { ctl.clearBusy(); return; } @@ -338,7 +338,7 @@ async function boot() { notify(pre.reason || 'Push cancelled'); return; } - if (TAURI.has) { setBusy('Pushing…'); await TAURI.invoke('git_push', {}); } + setBusy('Pushing…'); await TAURI.invoke('git_push', {}); await runHook('onPush', hookData); notify('Pushed'); await Promise.allSettled([hydrateStatus(), hydrateCommits()]); @@ -347,9 +347,7 @@ async function boot() { } async function openDocs() { - if (TAURI.has) { - try { await TAURI.invoke('open_docs', {}); return; } catch { /* fall back */ } - } + try { await TAURI.invoke('open_docs', {}); } catch { /* fall back */ } try { window.open(WIKI_URL, '_blank', 'noopener'); } catch (e) { console.error('Unable to open docs:', e); notify('Unable to open docs'); } } @@ -364,7 +362,6 @@ async function boot() { case 'docs': console.log('Action: docs'); await openDocs(); break; case 'show-output-log': console.log('Action: show-output-log'); - if (!TAURI.has) { notify('Output Log is available in the desktop app'); break; } try { await TAURI.invoke('open_output_log_window', {}); } catch (e) { console.error('Failed to open Output Log:', e); notify('Failed to open Output Log'); } break; @@ -374,7 +371,6 @@ async function boot() { case 'repo-edit-gitignore': case 'repo-edit-gitattributes': { console.log('Action:', id); - if (!TAURI.has) { notify('Open this in the desktop app to edit repository files'); break; } const name = id === 'repo-edit-gitignore' ? '.gitignore' : '.gitattributes'; try { await TAURI.invoke('open_repo_dotfile', { name }); } catch (e) { console.error(`Could not open ${name}:`, e); notify(`Could not open ${name}`); } @@ -382,14 +378,12 @@ async function boot() { } case 'lfs-settings': openSettings('lfs'); break; case 'check_updates': - if (!TAURI.has) { notify('Update checks are available in the desktop app'); break; } try { const hasUpdate = await TAURI.invoke('check_for_updates', {}); if (!hasUpdate) notify('Already up to date'); } catch (e) { console.error('Update check failed:', e); notify('Update check failed'); } break; case '__plugin_menu_action__': { - if (!TAURI.has) { notify('Plugin actions are available in the desktop app'); break; } const pluginId = typeof payload?.pluginId === 'string' ? payload.pluginId.trim() : ''; const actionId = typeof payload?.actionId === 'string' ? payload.actionId.trim() : ''; if (!pluginId || !actionId) { @@ -409,7 +403,7 @@ async function boot() { } break; } - case 'exit': if (TAURI.has) { TAURI.invoke('exit_app', {}).catch(() => {}); } break; + case 'exit': TAURI.invoke('exit_app', {}).catch(() => {}); break; default: { if (!id) break; const handled = await runPluginAction(id); @@ -444,7 +438,6 @@ async function boot() { }; const clearBusy = () => { if (statusEl) statusEl.classList.remove('busy'); }; try { - if (!TAURI.has) return; setBusy('Undoing…'); await TAURI.invoke('git_undo_since_push', {}); notify('Undid unpushed commits'); @@ -531,8 +524,7 @@ async function boot() { }); // If backend reopened a repo before the webview was ready, sync initial state. - if (TAURI.has) { - TAURI.invoke('current_repo_path') + TAURI.invoke('current_repo_path') .then(async (p) => { const path = (p || '').trim(); if (!path) return; @@ -548,7 +540,6 @@ async function boot() { schedulePluginMenuRefresh(); }) .catch(() => {}); - } // backend status updates (footer) TAURI.listen?.('status:set', ({ payload }) => { @@ -566,17 +557,15 @@ async function boot() { if (focusInFlight) return focusInFlight; focusInFlight = (async () => { let doFetch = true; - if (TAURI.has) { - try { - const fields = await TAURI.invoke>('get_plugin_settings', { - pluginId: 'openvcs.git', - }); - const fetchSetting = (Array.isArray(fields) ? fields : []).find((field) => String(field?.id || '').trim() === 'fetch_on_focus'); - if (fetchSetting && typeof fetchSetting.value === 'boolean') { - doFetch = fetchSetting.value; - } - } catch {} - } + try { + const fields = await TAURI.invoke>('get_plugin_settings', { + pluginId: 'openvcs.git', + }); + const fetchSetting = (Array.isArray(fields) ? fields : []).find((field) => String(field?.id || '').trim() === 'fetch_on_focus'); + if (fetchSetting && typeof fetchSetting.value === 'boolean') { + doFetch = fetchSetting.value; + } + } catch {} if (doFetch) { await fetchCurrentRemoteOnly({ hydrate: false }); } @@ -602,7 +591,7 @@ async function boot() { const headPollMs = 15000; const scheduleHeadPoll = () => { window.setTimeout(async () => { - if (!TAURI.has || !state.hasRepo || document.visibilityState !== 'visible' || !document.hasFocus()) { + if (!isTauriRuntimeAvailable() || !state.hasRepo || document.visibilityState !== 'visible' || !document.hasFocus()) { return scheduleHeadPoll(); } if (headPollInFlight) { @@ -677,10 +666,6 @@ async function boot() { /** Loads persisted global settings for bootstrap-time features such as theming and monitoring. */ async function loadInitialGlobalSettings(): Promise { - if (!TAURI.has) { - return null; - } - try { return await TAURI.invoke('get_global_settings'); } catch { diff --git a/Frontend/src/scripts/plugins.ts b/Frontend/src/scripts/plugins.ts index 58cd3f1..89d719d 100644 --- a/Frontend/src/scripts/plugins.ts +++ b/Frontend/src/scripts/plugins.ts @@ -603,11 +603,6 @@ export async function invokePluginAction( actionId: string, payload?: Record, ): Promise { - if (!TAURI.has) { - notify('Plugin actions are available in the desktop app'); - return null; - } - const result = await TAURI.invoke('invoke_plugin_action', { pluginId, actionId, @@ -1152,8 +1147,6 @@ export async function initPlugins(): Promise { ensurePluginsMenuPlaceholder(); - if (!TAURI.has) return; - resetPluginRuntime(); ensurePluginsMenuPlaceholder(); @@ -1188,7 +1181,6 @@ export async function initPlugins(): Promise { /** Reloads plugins by resetting and reinitializing the plugin runtime. */ export async function reloadPlugins(): Promise { installGlobalApi(); - if (!TAURI.has) return; initialized = false; await initPlugins(); } diff --git a/Frontend/src/scripts/themes.ts b/Frontend/src/scripts/themes.ts index b935716..fa11d62 100644 --- a/Frontend/src/scripts/themes.ts +++ b/Frontend/src/scripts/themes.ts @@ -301,22 +301,6 @@ export function getCurrentMode(): 'system' | 'light' | 'dark' { /** Refreshes available themes from backend and plugin registries. */ export async function refreshAvailableThemes(): Promise { const pluginSummaries = getRegisteredThemeSummaries(); - if (!TAURI.has) { - const others: ThemeSummary[] = []; - const seen = new Set([DEFAULT_THEME_ID, DEFAULT_LIGHT_THEME_ID, DEFAULT_DARK_THEME_ID]); - for (const item of Array.isArray(pluginSummaries) ? pluginSummaries : []) { - if (!item) continue; - const summary = sanitizeSummary(item); - const norm = summary.id.toLowerCase(); - if (seen.has(norm)) continue; - seen.add(norm); - others.push(summary); - } - others.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })); - availableThemes = [defaultLightSummary(), defaultDarkSummary(), ...others]; - fetchedThemes = true; - return availableThemes; - } try { const list = await TAURI.invoke('list_themes'); @@ -388,7 +372,7 @@ export async function selectThemePack( if (paired) target = paired; } - if (!TAURI.has || isBuiltInDefaultThemeId(target)) { + if (isBuiltInDefaultThemeId(target)) { activeThemeId = isBuiltInDefaultThemeId(target) ? target : defaultThemeIdForMode(desiredMode); activeThemePackId = activeThemeId; activeStyles = null; diff --git a/Frontend/src/scripts/ui/layout.ts b/Frontend/src/scripts/ui/layout.ts index 8452bed..ea47421 100644 --- a/Frontend/src/scripts/ui/layout.ts +++ b/Frontend/src/scripts/ui/layout.ts @@ -58,20 +58,16 @@ export function setTheme(theme: 'dark'|'light'|'system') { export function toggleTheme() { const next = (prefs.theme === 'dark' ? 'light' : 'dark'); // Persist to native settings when available, then apply to UI - if (TAURI.has) { - (async () => { - try { - const cur = await TAURI.invoke('get_global_settings'); - if (cur && typeof cur === 'object') { - cur.general = { ...(cur.general || {}), theme: next }; - await TAURI.invoke('set_global_settings', { cfg: cur }); - } - } catch {} - setTheme(next); - })(); - } else { + (async () => { + try { + const cur = await TAURI.invoke('get_global_settings'); + if (cur && typeof cur === 'object') { + cur.general = { ...(cur.general || {}), theme: next }; + await TAURI.invoke('set_global_settings', { cfg: cur }); + } + } catch {} setTheme(next); - } + })(); } /** Switches the active center tab and updates related UI state. */ diff --git a/Frontend/src/scripts/ui/menubar.ts b/Frontend/src/scripts/ui/menubar.ts index d748972..bb8f012 100644 --- a/Frontend/src/scripts/ui/menubar.ts +++ b/Frontend/src/scripts/ui/menubar.ts @@ -1,7 +1,7 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later -import { TAURI } from '../lib/tauri'; +import { TAURI, isTauriRuntimeAvailable } from '../lib/tauri'; type MenuAction = (id: string, payload?: { pluginId?: string; actionId?: string }) => void | Promise; const MENU_CLOSE_MS = 130; @@ -48,7 +48,7 @@ export function clearPluginMenubarMenus(): void { export async function refreshPluginMenubarMenus(): Promise { clearPluginMenubarMenus(); - if (!TAURI.has) return; + if (!isTauriRuntimeAvailable()) return; let menus: PluginMenuPayload[] = []; try {