Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/praisonai-desktop/frontend/tests/buttons.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {} });
},
Expand Down Expand Up @@ -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();
Expand Down
44 changes: 41 additions & 3 deletions src/praisonai-desktop/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -952,6 +955,38 @@ <h2>Fine-tune a model</h2>
});
}

/**
* 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='<div class="confirm-box" role="dialog" aria-modal="true">'
+'<p></p><input type="text" class="txt"/>'
+'<div class="row"><button type="button" class="ghost cx"></button>'
+'<button type="button" class="ok"></button></div></div>';
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. */
Expand Down Expand Up @@ -1049,7 +1084,7 @@ <h2>Fine-tune a model</h2>
};
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})});
Expand Down Expand Up @@ -1954,11 +1989,14 @@ <h2>Fine-tune a model</h2>
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; }
// 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){ alert(r.error); return; }
if(!r.ok){ await askConfirm(r.error,{danger:false,ok:'OK',cancel:'Dismiss'}); return; }
Comment on lines +1995 to +1999

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Escape closes both overlays

When the user presses Escape to dismiss an MCP error dialog, both document-level Escape handlers run, causing the foreground dialog and the underlying MCP overlay to close and discarding the in-progress add-server form.

runAction(def);
};
overlay.classList.add('open');
Expand Down
Loading