From 0c0d7ab6c64ee7a5f4aa28e8c3b8935e5380ac4b 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:54:38 +0000
Subject: [PATCH 1/2] fix: replace non-functional alert()/prompt() in desktop
UI (fixes #4503)
WKWebView under Tauri has no JS dialog panel, so prompt() returns null
and alert() shows nothing. This made "Move to project" inert and hid MCP
add errors. Add an askText() helper mirroring askConfirm(), and route
the three sites through askText/toast. Update the button test stubs to
model the real platform (prompt->null, alert throws) and add a
contextmenu test asserting a POST /project request is issued.
Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
---
.../frontend/tests/buttons.test.mjs | 25 ++++++++++-
src/praisonai-desktop/ui/index.html | 41 +++++++++++++++++--
2 files changed, 61 insertions(+), 5 deletions(-)
diff --git a/src/praisonai-desktop/frontend/tests/buttons.test.mjs b/src/praisonai-desktop/frontend/tests/buttons.test.mjs
index 13efd254d..9a93ff7ad 100644
--- a/src/praisonai-desktop/frontend/tests/buttons.test.mjs
+++ b/src/praisonai-desktop/frontend/tests/buttons.test.mjs
@@ -63,8 +63,11 @@ async function boot() {
w.requestAnimationFrame = (cb) => setTimeout(cb, 0);
w.navigator.clipboard = { writeText: async () => {} };
w.confirm = () => true;
- w.prompt = () => 'Research';
- w.alert = () => {};
+ // This webview has no JS dialog panel: prompt returns null and alert
+ // shows nothing. Modelling the real platform is the point -- a stub that
+ // returns 'x' hid that "Move to project" issued no request at all.
+ w.prompt = () => null;
+ w.alert = () => { throw new Error('no dialog panel here'); };
w.scrollTo = () => {};
Object.defineProperty(w.HTMLElement.prototype, 'scrollIntoView', { value() {} });
},
@@ -185,6 +188,24 @@ test('Engine log opens and shows lines', async () => {
assert.match(doc.getElementById('panel').textContent, /turn start/);
});
+test('right-click on a chat moves it to a project via the in-app prompt', async () => {
+ const { doc, window, calls } = await boot();
+ await settle();
+ const row = doc.querySelector('#chats .chat, #chats > div');
+ assert.ok(row, 'no chat row rendered');
+ // window.prompt returns null here, so if the handler used it this issues
+ // nothing. The in-app askText panel must appear instead.
+ row.dispatchEvent(new window.MouseEvent('contextmenu', { bubbles: true, cancelable: true }));
+ await settle();
+ const inp = doc.querySelector('.confirm-back .txt');
+ assert.ok(inp, 'no in-app text prompt shown (window.prompt returned null)');
+ inp.value = 'Research';
+ click([...doc.querySelectorAll('.confirm-back .ok')].pop());
+ await settle();
+ assert.ok(calls.some((c) => c.startsWith('POST /project/')),
+ 'moving to a project issued no request');
+});
+
test('every shell button produces its own observable effect', async () => {
const { doc, calls } = await boot();
await settle();
diff --git a/src/praisonai-desktop/ui/index.html b/src/praisonai-desktop/ui/index.html
index 9e67f1c62..01df90c5a 100644
--- a/src/praisonai-desktop/ui/index.html
+++ b/src/praisonai-desktop/ui/index.html
@@ -20,6 +20,9 @@
.confirm-box p{margin:0 0 1rem;font-size:.88rem;line-height:1.5}
.confirm-box .row{display:flex;gap:.5rem;justify-content:flex-end}
.confirm-box .ok.danger{background:var(--bad)}
+.confirm-box .txt{width:100%;box-sizing:border-box;margin:0 0 1rem;background:var(--ground);
+ color:var(--ink);border:1px solid var(--rule);border-radius:6px;padding:.4rem .55rem;
+ font:inherit;font-size:.88rem}
.updbar .updx{font-size:.72rem;padding:.25rem .6rem}
/* In normal flow above the composer. Fixed-position at bottom:18px put it
directly on top of the text field -- and a composer that grows to ten
@@ -952,6 +955,38 @@
Fine-tune a model
});
}
+/**
+ * In-app text prompt, because `window.prompt` does not work here either.
+ *
+ * The same WKWebView that has no confirm panel has no prompt panel, so
+ * `prompt()` returns null without ever showing anything -- which is why the
+ * "Move to project" menu item issued no request at all. Mirrors askConfirm:
+ * resolves to the entered string, or null if cancelled.
+ */
+function askText(message, value='', {ok='OK', cancel='Cancel'}={}){
+ return new Promise(resolve=>{
+ const back=document.createElement('div'); back.className='confirm-back';
+ back.innerHTML='';
+ back.querySelector('p').textContent=message;
+ const inp=back.querySelector('.txt'); inp.value=value;
+ const okBtn=back.querySelector('.ok'), cxBtn=back.querySelector('.cx');
+ okBtn.textContent=ok; cxBtn.textContent=cancel;
+ const close=v=>{ back.remove(); document.removeEventListener('keydown',key); resolve(v); };
+ const key=e=>{
+ if(e.key==='Escape'){ e.preventDefault(); close(null); }
+ else if(e.key==='Enter'){ e.preventDefault(); close(inp.value); }
+ };
+ okBtn.onclick=()=>close(inp.value); cxBtn.onclick=()=>close(null);
+ back.onclick=e=>{ if(e.target===back) close(null); };
+ document.addEventListener('keydown',key);
+ document.body.appendChild(back);
+ inp.focus(); inp.select();
+ });
+}
+
let toastTimer=null;
/** Brief, non-blocking confirmation. Silence after an action the user did not
* explicitly ask for -- a paste turning into a chip -- reads as a bug. */
@@ -1049,7 +1084,7 @@ Fine-tune a model
};
d.oncontextmenu=async ev=>{
ev.preventDefault();
- const name=prompt('Move to project (blank to remove):', c.project||'');
+ const name=await askText('Move to project (blank to remove):', c.project||'');
if(name===null) return;
await fetch('http://127.0.0.1:'+PORT+'/project/'+c.id,{method:'POST',
headers:{'content-type':'application/json'},body:JSON.stringify({project:name})});
@@ -1954,11 +1989,11 @@ Fine-tune a model
panel.querySelector('#m-add').onclick=async()=>{
const name=panel.querySelector('#m-name').value.trim();
const cmd=panel.querySelector('#m-cmd').value.trim();
- if(!name){ alert('A name is required.'); return; }
+ if(!name){ toast('A name is required.'); return; }
const r=await (await fetch('http://127.0.0.1:'+PORT+'/mcp',{method:'POST',
headers:{'content-type':'application/json'},
body:JSON.stringify({action:'add',name,command:cmd,enabled:false})})).json();
- if(!r.ok){ alert(r.error); return; }
+ if(!r.ok){ toast(r.error); return; }
runAction(def);
};
overlay.classList.add('open');
From 8cf894817e952ee9a6cea1212ec61cf7239abbe8 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:44:40 +0000
Subject: [PATCH 2/2] fix: surface MCP add errors via in-app dialog, not dimmed
toast
The toast intentionally paints below the settings scrim (see layout.test
'the settings scrim covers the toast'), so blank-name and backend-error
feedback shown while the MCP overlay is open was dimmed and unreadable --
the same invisible-feedback bug this PR set out to fix. Route both MCP
error paths through askConfirm, whose .confirm-back (z-index 90) paints
above the overlay (z-index 80).
Co-authored-by: Mervin Praison
---
src/praisonai-desktop/ui/index.html | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/praisonai-desktop/ui/index.html b/src/praisonai-desktop/ui/index.html
index 01df90c5a..edcf1b9b1 100644
--- a/src/praisonai-desktop/ui/index.html
+++ b/src/praisonai-desktop/ui/index.html
@@ -1989,11 +1989,14 @@ Fine-tune a model
panel.querySelector('#m-add').onclick=async()=>{
const name=panel.querySelector('#m-name').value.trim();
const cmd=panel.querySelector('#m-cmd').value.trim();
- if(!name){ toast('A name is required.'); return; }
+ // The toast intentionally sits below the settings scrim, so it is dimmed
+ // and unreadable while this overlay is open. Errors that occur here must
+ // surface through the in-app dialog, which paints above the scrim.
+ if(!name){ await askConfirm('A name is required.',{danger:false,ok:'OK',cancel:'Dismiss'}); return; }
const r=await (await fetch('http://127.0.0.1:'+PORT+'/mcp',{method:'POST',
headers:{'content-type':'application/json'},
body:JSON.stringify({action:'add',name,command:cmd,enabled:false})})).json();
- if(!r.ok){ toast(r.error); return; }
+ if(!r.ok){ await askConfirm(r.error,{danger:false,ok:'OK',cancel:'Dismiss'}); return; }
runAction(def);
};
overlay.classList.add('open');