From f9cdb9094ca700b8af5c7e79e81c010be75f8f80 Mon Sep 17 00:00:00 2001 From: "praisonai-triage-agent[bot]" <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:39:29 +0000 Subject: [PATCH 1/2] fix: persist launch_at_login's honest result, not the request (fixes #4498) The /settings handler wrote the requested launch_at_login value first and only attached set_launch_at_login's result to the response -- which nothing reads. Off macOS (and in a macOS checkout) registration always returns {"enabled": false}, so the toggle rendered on, persisted, and survived restarts while no login item existed. Now the platform action runs first and the persisted value is coerced to what actually happened. Co-authored-by: MervinPraison --- src/praisonai-desktop/engine/server.py | 13 ++- .../engine/test_portability.py | 82 +++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/praisonai-desktop/engine/server.py b/src/praisonai-desktop/engine/server.py index b410c5f5e..598ec02ef 100644 --- a/src/praisonai-desktop/engine/server.py +++ b/src/praisonai-desktop/engine/server.py @@ -1674,11 +1674,16 @@ def do_POST(self): except (ValueError, TypeError): self.send_error(400) return - saved = save_settings(patch) if "launch_at_login" in patch: - saved = dict(saved) - saved["launch_at_login_result"] = set_launch_at_login( - bool(patch["launch_at_login"])) + # Persist what actually happened, not what was asked. Writing + # the request first made the toggle report a login item that + # was never registered -- and survive restarts saying so. + result = set_launch_at_login(bool(patch["launch_at_login"])) + patch = {**patch, "launch_at_login": bool(result.get("enabled"))} + saved = dict(save_settings(patch)) + saved["launch_at_login_result"] = result + else: + saved = save_settings(patch) self._json(saved) return diff --git a/src/praisonai-desktop/engine/test_portability.py b/src/praisonai-desktop/engine/test_portability.py index 211b4a0b9..219364784 100644 --- a/src/praisonai-desktop/engine/test_portability.py +++ b/src/praisonai-desktop/engine/test_portability.py @@ -689,5 +689,87 @@ def test_the_vocabulary_is_the_eleven_events_expected(self): self.assertEqual(self._emitted_events(), self.EXPECTED) +class LaunchAtLogin(unittest.TestCase): + """The toggle must persist what actually happened, not what was asked. + + Registering a login item only works in the installed macOS .app bundle: + `set_launch_at_login` returns {"enabled": False} everywhere else -- every + Windows and Linux user, and any macOS user running from a checkout. The + handler used to save the *request* and merely attach the honest result to + the response, which nothing read. So the toggle rendered on, persisted, and + survived restarts while no login item existed anywhere. + """ + + def setUp(self): + import io + + self.home = pathlib.Path(tempfile.mkdtemp(prefix="praison-launch-")) + self._data_dir = server.DATA_DIR + self._settings_path = server.SETTINGS_PATH + self._set = server.set_launch_at_login + server.DATA_DIR = self.home + server.SETTINGS_PATH = self.home / "settings.json" + self._io = io + + def tearDown(self): + server.DATA_DIR = self._data_dir + server.SETTINGS_PATH = self._settings_path + server.set_launch_at_login = self._set + shutil.rmtree(self.home, ignore_errors=True) + + def _post_settings(self, patch): + """Drive the real /settings POST handler and return its JSON reply.""" + body = json.dumps(patch).encode() + + class FakeHandler(server.Handler): + def __init__(self): + self.path = "/settings" + self.headers = {"Content-Length": str(len(body))} + self.rfile = self._io_module.BytesIO(body) + self.wfile = self._io_module.BytesIO() + + def send_response(self, *_a, **_k): + pass + + def send_header(self, *_a, **_k): + pass + + def end_headers(self): + pass + + FakeHandler._io_module = self._io + handler = FakeHandler() + handler.do_POST() + raw = handler.wfile.getvalue() + return json.loads(raw) if raw else {} + + def test_a_request_that_could_not_register_is_not_persisted_as_on(self): + # Stub the platform action to the answer every non-bundle host gives. + server.set_launch_at_login = lambda on: { + "ok": False, "enabled": False, + "message": "Only available in the installed app."} + + reply = self._post_settings({"launch_at_login": True}) + + self.assertFalse(reply.get("launch_at_login"), + "the toggle reported on though nothing was registered") + self.assertEqual( + reply.get("launch_at_login_result", {}).get("message"), + "Only available in the installed app.", + "the response dropped the explanation for why it did not stick") + self.assertFalse( + server.load_settings().get("launch_at_login"), + "the un-registered login item survived to the next launch") + + def test_a_request_that_registered_is_persisted_as_on(self): + server.set_launch_at_login = lambda on: {"ok": True, "enabled": bool(on)} + + reply = self._post_settings({"launch_at_login": True}) + + self.assertTrue(reply.get("launch_at_login")) + self.assertTrue(server.load_settings().get("launch_at_login"), + "a real registration did not persist") + + if __name__ == "__main__": unittest.main(verbosity=2) From 590beb84dcb7edfeb0c2308cf596de51db235ab3 Mon Sep 17 00:00:00 2001 From: "praisonai-triage-agent[bot]" <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:32:06 +0000 Subject: [PATCH 2/2] fix: reconcile launch_at_login toggle from server response in UI The settings client optimistically set launch_at_login=true and ignored the POST response, so when the OS declined to register a login item the toggle stayed on for the session even though the server persisted false. saveCfg now reads the response and reconciles any keys the server wrote back, and the launch_at_login handler surfaces the explanation. Co-authored-by: Mervin Praison --- src/praisonai-desktop/ui/index.html | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/praisonai-desktop/ui/index.html b/src/praisonai-desktop/ui/index.html index 9e67f1c62..fb73636f7 100644 --- a/src/praisonai-desktop/ui/index.html +++ b/src/praisonai-desktop/ui/index.html @@ -1832,8 +1832,19 @@

Fine-tune a model

async function saveCfg(patch){ CFG={...CFG,...patch}; applyPrefs(CFG); cfg = {...cfg, ...patch}; - await fetch('http://127.0.0.1:'+PORT+'/settings',{method:'POST', - headers:{'content-type':'application/json'},body:JSON.stringify(patch)}); + let saved=null; + try{ + saved=await (await fetch('http://127.0.0.1:'+PORT+'/settings',{method:'POST', + headers:{'content-type':'application/json'},body:JSON.stringify(patch)})).json(); + }catch{} + // The server persists what actually happened, not what we asked. Reconcile + // any key it wrote back so the current session never shows a value that was + // never saved -- e.g. "Open at login" the OS refused to register. + if(saved && typeof saved==='object'){ + for(const k of Object.keys(patch)) if(k in saved){ CFG[k]=saved[k]; cfg[k]=saved[k]; } + applyPrefs(CFG); + } + return saved; } function control(def, onChange){ @@ -1985,10 +1996,18 @@

Fine-tune a model

err.style.display='none'; if(def.confirm && def.confirm.when(cfgGet(def.key),nv) && !await askConfirm(def.confirm.message,{ok:'Continue'})) { renderSettings(); return; } - await saveCfg({[def.key]:nv}); + const saved=await saveCfg({[def.key]:nv}); if(def.key==='theme') applyTheme(nv); if(def.key==='model') modelName.textContent=nv; renderSettings(); + // If the OS declined to register the login item, the toggle has already + // snapped back to the persisted value above; tell the user why. + if(def.key==='launch_at_login' && saved && saved.launch_at_login_result + && nv && !saved.launch_at_login_result.enabled){ + const r=document.getElementById('setting-launch_at_login'); + const w=r&&r.querySelector('.warn'); + if(w){ w.textContent=saved.launch_at_login_result.message||'Not available.'; w.style.display=''; } + } }; const multiline = def.control.kind==='text' && def.control.multiline;