fix(desktop): first-run shortcut prompt + uninstaller folder deletion

Bug 1 (shortcut prompt only showed on the second launch): a native rfd
dialog spawned from the Rust startup thread has no parent window and got
buried behind the main window on first launch. Move the prompt to the web
UI: add shortcut_should_ask / shortcut_decide commands; the frontend asks
once via window.confirm after load (always on top). The startup thread now
only self-heals the shortcut path when already enabled.

Bug 2 (uninstall.bat could not delete the folder): kill ANY process whose
exe lives inside the bundle (not just GenericAgent/python by name), and run
the folder delete from a detached temp .bat that retries the recursive
remove ~20x to cover the window where uninstall.bat releases its handle.
This commit is contained in:
yiqi-017 committed 2026-06-21 23:18:29 +08:00
1 parent 15c59dd1e7
commit 4e1fa78ecf
5 files changed
+83 -31

No files matched your search

@@ -62,10 +62,10 @@ foreach ($port in 14168, 8900) {
}
}
# Kill any remaining GenericAgent.exe / python.exe whose image is inside this bundle
# (e.g. the desktop shell itself, or a child python not bound to the two ports).
foreach ($p in (Get-Process -ErrorAction SilentlyContinue | Where-Object {
$_.Name -in @('GenericAgent', 'python', 'pythonw') })) {
# Kill ANY process whose executable image lives inside this bundle, regardless of name
# (GenericAgent.exe, python.exe, or any child tool it spawned). Limiting to the bundle path
# means we never touch a second installed copy.
foreach ($p in (Get-Process -ErrorAction SilentlyContinue)) {
if (Path-IsInsideBundle $p.Path) {
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
Write-Info "killed PID $($p.Id) ($($p.ProcessName))"
@@ -104,11 +104,24 @@ if (Test-Path -LiteralPath $settings) {
}
# ── 4. Schedule deletion of the bundle folder ────────────────────────────────
# The folder cannot remove itself while this script runs from inside it. Spawn a
# detached cmd that waits for our process tree to exit, then deletes the folder.
# The folder cannot remove itself while this script (and the uninstall.bat that
# launched it) run from inside it. Spawn a detached cmd that waits for our process
# tree to fully exit, then retries the delete a few times in case a handle lingers.
Write-Step "Scheduling removal of the bundle folder"
$cmd = "cd /d `"$env:TEMP`" & ping 127.0.0.1 -n 3 >nul & rd /s /q `"$bundle`""
Start-Process -FilePath "cmd.exe" -ArgumentList "/c", $cmd -WindowStyle Hidden | Out-Null
$deleter = @"
cd /d "%TEMP%"
for /l %%i in (1,1,20) do (
rd /s /q "$bundle" 2>nul
if not exist "$bundle" goto done
ping 127.0.0.1 -n 2 >nul
)
:done
"@
$deleterPath = Join-Path $env:TEMP ("ga_uninstall_{0}.bat" -f ([guid]::NewGuid().ToString('N')))
Set-Content -LiteralPath $deleterPath -Value $deleter -Encoding ASCII
# Start detached so it survives this script + uninstall.bat exiting. The retry loop
# (20 tries x ~1s) covers the brief window where the parent processes release handles.
Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "`"$deleterPath`"" -WindowStyle Hidden | Out-Null
Write-Ok "bundle folder will be deleted after exit: $bundle"
Write-Host ""
@@ -17,6 +17,8 @@
"allow-start-bridge",
"allow-start-bridge-with-config",
"allow-get-config",
"allow-export-mykey"
"allow-export-mykey",
"allow-shortcut-should-ask",
"allow-shortcut-decide"
]
}
@@ -17,3 +17,13 @@ commands.allow = ["get_config"]
identifier = "allow-export-mykey"
description = "Save mykey.py via native file dialog."
commands.allow = ["export_mykey"]
[[permission]]
identifier = "allow-shortcut-should-ask"
description = "Check whether to show the first-run desktop-shortcut prompt."
commands.allow = ["shortcut_should_ask"]
[[permission]]
identifier = "allow-shortcut-decide"
description = "Persist the user's desktop-shortcut choice and create it if enabled."
commands.allow = ["shortcut_decide"]
+33 -22
View File
@@ -221,33 +221,44 @@ fn ensure_desktop_shortcut() {
#[cfg(not(windows))]
fn ensure_desktop_shortcut() {}
/// First-run shortcut handling. Windows portable bundles only. Asks once via a native dialog,
/// remembers the choice, and self-heals the shortcut path on every enabled launch.
/// First-run shortcut handling. Windows portable bundles only. Self-heals the shortcut path on
/// every enabled launch (cheap, no UI). The first-run ASK is driven by the frontend (see the
/// `shortcut_should_ask` / `shortcut_decide` commands): a native dialog from this background
/// startup thread has no parent window and gets buried behind the main window on first launch,
/// so the prompt is owned by the web UI instead, which always renders on top.
fn maybe_setup_shortcut() {
// Only for the self-contained Windows portable bundle; skip dev builds and other platforms.
#[cfg(not(windows))]
{ return; }
#[cfg(windows)]
{
if bundle_root().is_none() {
return;
}
match read_shortcut_pref() {
Some(true) => ensure_desktop_shortcut(),
Some(false) => {}
None => {
let create = rfd::MessageDialog::new()
.set_title("GenericAgent")
.set_description("是否在桌面创建 GenericAgent 快捷方式?\nCreate a desktop shortcut for GenericAgent?")
.set_buttons(rfd::MessageButtons::YesNo)
.show();
let enabled = matches!(create, rfd::MessageDialogResult::Yes);
write_shortcut_pref(enabled);
if enabled {
ensure_desktop_shortcut();
}
}
// Only self-heal when the user already opted in. Never prompt here.
if read_shortcut_pref() == Some(true) {
ensure_desktop_shortcut();
}
}
}
/// Frontend asks whether to show the first-run "create desktop shortcut?" prompt.
/// True only on a Windows portable bundle whose preference has never been set.
#[tauri::command]
fn shortcut_should_ask() -> bool {
#[cfg(windows)]
{
return bundle_root().is_some() && read_shortcut_pref().is_none();
}
#[cfg(not(windows))]
{ false }
}
/// Frontend reports the user's choice. Persists it and creates the shortcut when enabled.
#[tauri::command]
fn shortcut_decide(create: bool) {
write_shortcut_pref(create);
#[cfg(windows)]
{
if create {
ensure_desktop_shortcut();
}
}
}
@@ -651,7 +662,7 @@ pub fn run() {
let _ = w.set_focus();
}
}))
.invoke_handler(tauri::generate_handler![start_bridge_with_config, start_bridge, get_config, export_mykey])
.invoke_handler(tauri::generate_handler![start_bridge_with_config, start_bridge, get_config, export_mykey, shortcut_should_ask, shortcut_decide])
.setup(move |app| {
// Show the loading window immediately so the first-run prepare isn't a blank screen.
// The window starts on loading.html (a local page), so no "connection refused" flash.
+16
View File
@@ -333,6 +333,7 @@ const I18N = {
'customPreset.editTitle': '编辑',
'builtinPreset.restoreBtn': '恢复默认预设',
'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py)', 'set.exportMykey': '导出当前模型配置', 'set.serviceManager': '后台服务管理',
'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?',
'appearance.light': '浅色', 'appearance.dark': '深色',
'set.noModels': '暂无模型,点击下方添加',
'lang.zh': '简体中文', 'lang.en': 'English',
@@ -500,6 +501,7 @@ const I18N = {
'customPreset.editTitle': 'Edit',
'builtinPreset.restoreBtn': 'Restore defaults',
'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.serviceManager': 'Service manager',
'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?',
'appearance.light': 'Light', 'appearance.dark': 'Dark',
'set.noModels': 'No models yet — add one below',
'lang.zh': '简体中文', 'lang.en': 'English',
@@ -4966,6 +4968,20 @@ function showChanToast(title, detail, kind) {
// Preset form: 每字段独立内联提示(标题/Prompt 各自独立,互不串扰)
bindFieldInlineLimit(document.getElementById('cp-title'));
bindFieldInlineLimit(document.getElementById('cp-prompt'));
// First-run desktop-shortcut prompt (Windows portable bundle only). Driven from the web UI
// so the dialog always renders on top — a native dialog from the Rust startup thread had no
// parent window and got buried behind the main window on first launch.
maybeAskDesktopShortcut();
}
async function maybeAskDesktopShortcut() {
try {
const should = await window.ga.tauriInvoke('shortcut_should_ask');
if (!should) return;
const create = window.confirm(t('shortcut.askConfirm'));
await window.ga.tauriInvoke('shortcut_decide', { create });
} catch (_) { /* not in tauri / not a bundle — ignore */ }
}
if (document.readyState === 'loading') {