feat(startup): wire startup repo path from CLI to UI
The app now supports opening a repository at startup by reading a --repo argument and passing the path to the UI. A new startup repository state is managed in the backend and exposed via a tauri command and event, enabling the frontend to auto-open the repository when ready. The UI adds a small startup flow with retry handling to ensure the path is consumed once available. - Reads --repo or --repo=PATH and resolves relative paths - Signals UI to open startup repo via open-startup-repository - Frontend retries opening the repo until it succeeds
This commit is contained in:
+86
-3
@@ -31,9 +31,79 @@ use git::{
|
|||||||
stash_drop, stash_pop, stash_push, undo_last_commit, unlock_worktree, unstage_files,
|
stash_drop, stash_pop, stash_push, undo_last_commit, unlock_worktree, unstage_files,
|
||||||
update_remote,
|
update_remote,
|
||||||
};
|
};
|
||||||
use tauri::Manager;
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tauri::{Emitter, Manager};
|
||||||
use telemetry::{emit_frontend_log, emit_frontend_span, set_telemetry_enabled};
|
use telemetry::{emit_frontend_log, emit_frontend_span, set_telemetry_enabled};
|
||||||
|
|
||||||
|
struct StartupRepository(Mutex<Option<String>>);
|
||||||
|
|
||||||
|
fn repository_path_from_args(args: impl IntoIterator<Item = String>, cwd: &Path) -> Option<String> {
|
||||||
|
let mut args = args.into_iter();
|
||||||
|
let mut repository = None;
|
||||||
|
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
if arg == "--repo" {
|
||||||
|
repository = args.next();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(path) = arg.strip_prefix("--repo=") {
|
||||||
|
repository = Some(path.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !arg.starts_with('-') {
|
||||||
|
repository = Some(arg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.filter(|path| !path.trim().is_empty()).map(|path| {
|
||||||
|
let path = PathBuf::from(path);
|
||||||
|
if path.is_absolute() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
cwd.join(path)
|
||||||
|
}
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod startup_repository_tests {
|
||||||
|
use super::repository_path_from_args;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_direct_relative_repository_path() {
|
||||||
|
let path = repository_path_from_args(["projects/repo".to_string()], Path::new("/home/user"));
|
||||||
|
assert_eq!(path.as_deref(), Some("/home/user/projects/repo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_repo_option() {
|
||||||
|
let path = repository_path_from_args(
|
||||||
|
["--repo".to_string(), "/projects/repo".to_string()],
|
||||||
|
Path::new("/home/user"),
|
||||||
|
);
|
||||||
|
assert_eq!(path.as_deref(), Some("/projects/repo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accepts_repo_equals_option() {
|
||||||
|
let path = repository_path_from_args(
|
||||||
|
["--repo=projects/repo".to_string()],
|
||||||
|
Path::new("/home/user"),
|
||||||
|
);
|
||||||
|
assert_eq!(path.as_deref(), Some("/home/user/projects/repo"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn take_startup_repository(state: tauri::State<'_, StartupRepository>) -> Option<String> {
|
||||||
|
state.0.lock().ok()?.take()
|
||||||
|
}
|
||||||
|
|
||||||
struct ConsoleLogger;
|
struct ConsoleLogger;
|
||||||
|
|
||||||
impl log::Log for ConsoleLogger {
|
impl log::Log for ConsoleLogger {
|
||||||
@@ -96,8 +166,19 @@ async fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let startup_repository = repository_path_from_args(
|
||||||
|
std::env::args().skip(1),
|
||||||
|
&std::env::current_dir().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
|
||||||
let builder = tauri::Builder::default()
|
let builder = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
||||||
|
if let Some(path) = repository_path_from_args(args.into_iter().skip(1), Path::new(&cwd)) {
|
||||||
|
if let Ok(mut pending) = app.state::<StartupRepository>().0.lock() {
|
||||||
|
*pending = Some(path);
|
||||||
|
}
|
||||||
|
let _ = app.emit("open-startup-repository", ());
|
||||||
|
}
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
let _ = app
|
let _ = app
|
||||||
.get_webview_window("main")
|
.get_webview_window("main")
|
||||||
@@ -112,6 +193,7 @@ async fn main() {
|
|||||||
})
|
})
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
.manage(StartupRepository(Mutex::new(startup_repository)))
|
||||||
.manage(SearchCancellationState::default())
|
.manage(SearchCancellationState::default())
|
||||||
.manage(commit_ai::CommitAiEngine::new())
|
.manage(commit_ai::CommitAiEngine::new())
|
||||||
.plugin(tauri_plugin_dialog::init());
|
.plugin(tauri_plugin_dialog::init());
|
||||||
@@ -226,7 +308,8 @@ async fn main() {
|
|||||||
close_splashscreen,
|
close_splashscreen,
|
||||||
set_telemetry_enabled,
|
set_telemetry_enabled,
|
||||||
emit_frontend_log,
|
emit_frontend_log,
|
||||||
emit_frontend_span
|
emit_frontend_span,
|
||||||
|
take_startup_repository
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, tick } from "svelte";
|
import { onDestroy, onMount, tick } from "svelte";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||||
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
|
||||||
@@ -275,6 +276,10 @@
|
|||||||
// ── State ──────────────────────────────────────────────────────────────────
|
// ── State ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let repoPath = "";
|
let repoPath = "";
|
||||||
|
let startupReady = false;
|
||||||
|
let pendingStartupRepoPath = "";
|
||||||
|
let startupRepositoryRetryTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
let unlistenStartupRepository: (() => void) | undefined;
|
||||||
let activeRepoPath = "";
|
let activeRepoPath = "";
|
||||||
let activeView: AppView = "management";
|
let activeView: AppView = "management";
|
||||||
let repoTabs: RepoTab[] = [];
|
let repoTabs: RepoTab[] = [];
|
||||||
@@ -543,6 +548,14 @@
|
|||||||
themeMediaQuery = window.matchMedia("(prefers-color-scheme: light)");
|
themeMediaQuery = window.matchMedia("(prefers-color-scheme: light)");
|
||||||
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
||||||
void runStartupSequence();
|
void runStartupSequence();
|
||||||
|
void listen("open-startup-repository", () => {
|
||||||
|
void receiveStartupRepository();
|
||||||
|
}).then((unlisten) => {
|
||||||
|
if (appShuttingDown) unlisten();
|
||||||
|
else unlistenStartupRepository = unlisten;
|
||||||
|
}).catch(() => {
|
||||||
|
// Browser preview has no Tauri event system.
|
||||||
|
});
|
||||||
void refreshDetectedExternalTools();
|
void refreshDetectedExternalTools();
|
||||||
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
void getVersion().then((version) => { appVersion = version; }).catch(() => { appVersion = ""; });
|
||||||
window.addEventListener("beforeunload", handleAppShutdown);
|
window.addEventListener("beforeunload", handleAppShutdown);
|
||||||
@@ -565,7 +578,9 @@
|
|||||||
window.removeEventListener("beforeunload", handleAppShutdown);
|
window.removeEventListener("beforeunload", handleAppShutdown);
|
||||||
window.removeEventListener("pagehide", handleAppShutdown);
|
window.removeEventListener("pagehide", handleAppShutdown);
|
||||||
unlistenCloseRequested?.();
|
unlistenCloseRequested?.();
|
||||||
|
unlistenStartupRepository?.();
|
||||||
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
|
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
|
||||||
|
if (startupRepositoryRetryTimer) clearTimeout(startupRepositoryRetryTimer);
|
||||||
Object.values(errorAutoHideStates).forEach((state) => {
|
Object.values(errorAutoHideStates).forEach((state) => {
|
||||||
if (state?.timer) clearTimeout(state.timer);
|
if (state?.timer) clearTimeout(state.timer);
|
||||||
});
|
});
|
||||||
@@ -642,6 +657,8 @@
|
|||||||
if (remainingSplashTime > 0) await wait(remainingSplashTime);
|
if (remainingSplashTime > 0) await wait(remainingSplashTime);
|
||||||
|
|
||||||
await closeStartupSplashscreen();
|
await closeStartupSplashscreen();
|
||||||
|
startupReady = true;
|
||||||
|
await receiveStartupRepository();
|
||||||
startBackgroundTimers();
|
startBackgroundTimers();
|
||||||
// Remote access can take seconds (offline networks, SSH negotiation,
|
// Remote access can take seconds (offline networks, SSH negotiation,
|
||||||
// credential helpers). It must never hold the startup screen hostage.
|
// credential helpers). It must never hold the startup screen hostage.
|
||||||
@@ -650,6 +667,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function receiveStartupRepository() {
|
||||||
|
try {
|
||||||
|
const path = await tracedInvoke<string | null>("take_startup_repository");
|
||||||
|
if (path) pendingStartupRepoPath = path;
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startupReady || !pendingStartupRepoPath) return;
|
||||||
|
if (isBusy) {
|
||||||
|
if (!startupRepositoryRetryTimer) {
|
||||||
|
startupRepositoryRetryTimer = setTimeout(() => {
|
||||||
|
startupRepositoryRetryTimer = undefined;
|
||||||
|
void receiveStartupRepository();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const path = pendingStartupRepoPath;
|
||||||
|
pendingStartupRepoPath = "";
|
||||||
|
await openRepo(path);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchOpenRepositoriesDuringStartup() {
|
async function fetchOpenRepositoriesDuringStartup() {
|
||||||
const paths = uniqueRepoPaths(repoTabs.map((tab) => tab.path));
|
const paths = uniqueRepoPaths(repoTabs.map((tab) => tab.path));
|
||||||
if (paths.length === 0 || backgroundFetchInFlight) return;
|
if (paths.length === 0 || backgroundFetchInFlight) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user