From 6df05aef88e1e73d196a561c6b5d9057b64fcd26 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:48:47 +0000 Subject: [PATCH] fix: report runAction failures in desktop app instead of failing silently (fixes #4502) Wrap the settings runAction body in runActionInner and catch failures with a toast, so Export/Check for updates/MCP no longer resolve into silence when the engine is unreachable. Export additionally throws on non-ok responses so a 500 cannot slip past, and the clipboard is only written on success -- a failed export can no longer masquerade as a backup via a stale paste. Co-authored-by: MervinPraison --- .../frontend/tests/engine-down.test.mjs | 31 +++++++++++++++++++ src/praisonai-desktop/ui/index.html | 17 ++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/praisonai-desktop/frontend/tests/engine-down.test.mjs b/src/praisonai-desktop/frontend/tests/engine-down.test.mjs index f2c2f7b16..f641b68af 100644 --- a/src/praisonai-desktop/frontend/tests/engine-down.test.mjs +++ b/src/praisonai-desktop/frontend/tests/engine-down.test.mjs @@ -77,6 +77,37 @@ for (const [id, label] of [['settings', 'Settings'], ['logsBtn', 'Engine log'], }); } +test('Export does not fake a backup when the engine is unreachable', async () => { + const writes = []; + const dom = new JSDOM(HTML, { + runScripts: 'dangerously', resources: 'usable', url: ORIGIN + '/', + beforeParse(w) { + w.__TAURI__ = { core: { invoke: async () => ({ + state: 'failed', reason: 'Engine crashed', + detail: 'exited before it was ready', tail: 'traceback' }) }, + event: { listen: async () => () => {} } }; + w.fetch = async () => { throw new TypeError('fetch failed'); }; + w.requestAnimationFrame = (cb) => setTimeout(cb, 0); + w.navigator.clipboard = { writeText: async (t) => { writes.push(t); } }; + w.confirm = () => true; w.prompt = () => 'x'; w.alert = () => {}; w.scrollTo = () => {}; + w.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {} }); + Object.defineProperty(w.HTMLElement.prototype, 'scrollIntoView', { value() {} }); + }, + }); + const { window } = dom; + await new Promise((r) => setTimeout(r, 400)); + + await window.runAction({ action: 'export' }); + await new Promise((r) => setTimeout(r, 20)); + + assert.deepEqual(writes, [], + 'the clipboard was written despite the export failing -- a stale paste would masquerade as a backup'); + assert.ok(window.document.getElementById('toast'), + 'the failed export said nothing'); + assert.match(window.document.getElementById('toast').textContent, /did not run|could not/i, + 'the failed export produced no error message'); +}); + test('no unhandled rejection escapes any of the three', async () => { const b = await bootFailed(); for (const id of ['settings', 'logsBtn', 'search']) { diff --git a/src/praisonai-desktop/ui/index.html b/src/praisonai-desktop/ui/index.html index 9e67f1c62..f5ae3c502 100644 --- a/src/praisonai-desktop/ui/index.html +++ b/src/praisonai-desktop/ui/index.html @@ -1900,10 +1900,23 @@

Fine-tune a model

async function runAction(def){ if(def.confirm && !await askConfirm(def.confirm.message,{ok:'Continue'})) return; + // A failed export that says nothing is worse than no export: the user pastes + // a stale clipboard into a file and believes they have a backup. Every branch + // is a bare fetch that rejects when the engine is unreachable, so report it. + try{ await runActionInner(def); } + catch(e){ toast('That did not run: '+(e && e.message ? e.message : e)); } +} +async function runActionInner(def){ if(def.action==='export'){ - const {chats}=await (await fetch('http://127.0.0.1:'+PORT+'/chats')).json(); + const cr=await fetch('http://127.0.0.1:'+PORT+'/chats'); + if(!cr.ok) throw new Error('Could not read conversations ('+cr.status+')'); + const {chats}=await cr.json(); const all=[]; - for(const c of chats) all.push(await (await fetch('http://127.0.0.1:'+PORT+'/chats/'+c.id)).json()); + for(const c of chats){ + const dr=await fetch('http://127.0.0.1:'+PORT+'/chats/'+c.id); + if(!dr.ok) throw new Error('Could not read a conversation ('+dr.status+')'); + all.push(await dr.json()); + } // A sandboxed webview blocks a download the page starts itself, so the // export goes to the clipboard and says so rather than failing silently. await navigator.clipboard.writeText(JSON.stringify(all,null,2));