fix(ui): persist session queue to sessionStorage across page refresh (#768)
Queued follow-up messages now survive page refresh. Persisted atomically in queueSessionMessage/shiftQueuedSessionMessage. On reload: if agent still active, queue is silently hydrated (done handler drains it); if idle, first entry is restored as a composer draft with a toast. Stale entries discarded. Fixes #660
This commit is contained in:
@@ -83,6 +83,44 @@ async function loadSession(sid){
|
||||
attachLiveStream(sid, activeStreamId, data.session.pending_attachments||[], {reconnecting:true});
|
||||
}
|
||||
}else{
|
||||
// Restore any queued message that survived page refresh via sessionStorage.
|
||||
// Only restore when the agent is idle — if active, the done handler drains it.
|
||||
if(typeof queueSessionMessage==='function'){
|
||||
try{
|
||||
const _storedQ=sessionStorage.getItem('hermes-queue-'+sid);
|
||||
if(_storedQ){
|
||||
const _entries=JSON.parse(_storedQ);
|
||||
if(Array.isArray(_entries)&&_entries.length){
|
||||
// Timestamp guard: drop entries older than the last assistant response
|
||||
// (means the agent already ran and the queue was already dispatched)
|
||||
const _lastMsg=(data.session.messages||[]).slice().reverse()
|
||||
.find(m=>m&&m.role==='assistant');
|
||||
const _lastAsst=_lastMsg?(_lastMsg.timestamp||_lastMsg._ts||0)*1000:0;
|
||||
const _fresh=_entries.filter(e=>!e._queued_at||e._queued_at>_lastAsst);
|
||||
if(_fresh.length){
|
||||
// Idle path: restore the first entry as a composer draft only. Do NOT
|
||||
// re-enqueue into SESSION_QUEUES — if we did, send() would dispatch the
|
||||
// draft directly (S.busy=false) and then setBusy(false) would drain the
|
||||
// same entry from the queue, causing a duplicate send. Any follow-up
|
||||
// entries (2..N) are discarded by design; the toast tells the user so.
|
||||
const _first=_fresh[0];
|
||||
const _msg=$&&$('msg');
|
||||
if(_msg&&_first.text&&!_msg.value){
|
||||
_msg.value=_first.text||'';
|
||||
if(typeof autoResize==='function') autoResize();
|
||||
if(typeof showToast==='function') showToast((_fresh.length>1?`${_fresh.length} queued messages restored (showing first)`:'Queued message restored')+' — review and send when ready');
|
||||
}
|
||||
// Clear persisted queue now that the draft is in the composer
|
||||
sessionStorage.removeItem('hermes-queue-'+sid);
|
||||
} else {
|
||||
sessionStorage.removeItem('hermes-queue-'+sid);
|
||||
}
|
||||
} else {
|
||||
sessionStorage.removeItem('hermes-queue-'+sid);
|
||||
}
|
||||
}
|
||||
}catch(_){sessionStorage.removeItem('hermes-queue-'+sid);}
|
||||
}
|
||||
updateQueueBadge(sid);
|
||||
S.messages=data.session.messages||[];
|
||||
const pendingMsg=typeof getPendingSessionMessage==='function'?getPendingSessionMessage(data.session):null;
|
||||
|
||||
13
static/ui.js
13
static/ui.js
@@ -10,14 +10,23 @@ function _getSessionQueue(sid, create=false){
|
||||
function queueSessionMessage(sid, payload){
|
||||
if(!sid||!payload) return 0;
|
||||
const q=_getSessionQueue(sid,true);
|
||||
q.push(payload);
|
||||
// Stamp created_at so the restore path can detect stale entries (agent already responded)
|
||||
const entry={...payload, _queued_at: Date.now()};
|
||||
q.push(entry);
|
||||
// Persist to sessionStorage so the queue survives page refresh
|
||||
try{ sessionStorage.setItem('hermes-queue-'+sid, JSON.stringify(q)); }catch(_){}
|
||||
return q.length;
|
||||
}
|
||||
function shiftQueuedSessionMessage(sid){
|
||||
const q=_getSessionQueue(sid,false);
|
||||
if(!q.length) return null;
|
||||
const next=q.shift();
|
||||
if(!q.length) delete SESSION_QUEUES[sid];
|
||||
if(!q.length){
|
||||
delete SESSION_QUEUES[sid];
|
||||
try{ sessionStorage.removeItem('hermes-queue-'+sid); }catch(_){}
|
||||
} else {
|
||||
try{ sessionStorage.setItem('hermes-queue-'+sid, JSON.stringify(q)); }catch(_){}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
function getQueuedSessionCount(sid){
|
||||
|
||||
Reference in New Issue
Block a user