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:
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"]
|
||||
@@ -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.
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in new issue
Block a user