diff --git a/web/app.js b/web/app.js index 6a11908..ede0132 100644 --- a/web/app.js +++ b/web/app.js @@ -91,10 +91,11 @@ function handleProgress(data) { setState(working ? "thinking" : "listening"); } function setTasksOpen(open) { + const wasOpen = !tasksPanel.hidden; tasksPanel.hidden = !open; tasksPanel.inert = !open; tasksPanel.classList.toggle("open", open); tasksBtn.setAttribute("aria-expanded", String(open)); - if (open) closeTasksBtn.focus(); else tasksBtn.focus(); + if (open) closeTasksBtn.focus(); else if (wasOpen) tasksBtn.focus(); } tasksBtn.setAttribute("aria-expanded", "false"); tasksBtn.addEventListener("click", () => setTasksOpen(!tasksPanel.classList.contains("open"))); diff --git a/web/chat.js b/web/chat.js index 2c7fff1..b1b39ee 100644 --- a/web/chat.js +++ b/web/chat.js @@ -204,7 +204,7 @@ }}; $('openPreferences').onclick = async () => {try {if (window.HopeSettings) {await HopeSettings.open('appearance'); return;} await HopeAppearance.load(); if (!HopeAuth.user) return; $('preferencesDialog').showModal();} catch(e) {notice(e.message);} }; $('closePreferences').onclick = () => $('preferencesDialog').close(); - $('preferencesForm').onsubmit = async event => {event.preventDefault(); try {const data = await api('/api/preferences', 'PUT', {revision: state.preferences.revision, palette: $('paletteSelect').value, appearance: $('appearanceSelect').value}); state.preferences = data.preferences; theme(data.preferences); $('preferencesMessage').textContent = 'Appearance saved.';} catch(e) {$('preferencesMessage').textContent = e.message;} }; + $('preferencesForm').onsubmit = async event => {event.preventDefault(); const epoch = state.epoch, user = HopeAuth.user?.id; try {if (!state.preferences || !user) return; const data = await api('/api/preferences', 'PUT', {revision: state.preferences.revision, palette: $('paletteSelect').value, appearance: $('appearanceSelect').value}); if (epoch !== state.epoch || user !== HopeAuth.user?.id) return; state.preferences = data.preferences; theme(data.preferences); $('preferencesMessage').textContent = 'Appearance saved.';} catch(e) {if (epoch === state.epoch && user === HopeAuth.user?.id) $('preferencesMessage').textContent = e.message;} }; async function refreshSelected(messages = true) {if (!state.conversation) return; const epoch = state.epoch, cid = state.conversation.id; const data = await api('/api/conversations/' + enc(cid)); if (epoch !== state.epoch || cid !== state.conversation?.id) return; selected(data.conversation); if (messages && terminal(state.run)) {const timeline = $('chatTimeline'), scroll = timeline.scrollTop; const incoming = new Map((data.messages || []).map(m => [m.id,m])); state.messages = state.messages.map(m => incoming.get(m.id) || m); for (const m of incoming.values()) if (!state.messages.some(old => old.id === m.id)) state.messages.push(m); render(); timeline.scrollTop = scroll;} } async function sync() {if (syncBusy || !HopeAuth.user || document.hidden) return; syncBusy = true; const epoch = state.epoch; try {const data = await api('/api/sync?after=' + state.syncCursor); if (epoch !== state.epoch) return; state.syncCursor = data.cursor; if (!data.changes?.length) return; for (const change of data.changes) {if (change.kind === 'preferences' && change.data && !$('preferencesDialog').open && !(window.HopeSettings?.isOpen && HopeSettings.section === 'appearance')) {state.preferences = change.data; theme(change.data);} if (change.kind === 'conversation' && change.id === state.conversation?.id && change.deleted) {reset(); notice('This conversation was deleted on another device.');}} await Promise.all([list(), workspaces(), api('/api/research').then(data => {if (epoch !== state.epoch) return; state.research = data.available === true && data.mode === 'hermes_tools'; $('researchMode').hidden = !state.research; $('researchModeLabel').hidden = !state.research;}), refreshSelected()]);} catch (error) {/* A subsequent foreground poll retries; never discard the draft. */} finally {syncBusy = false;}} document.addEventListener('visibilitychange', () => {if (!document.hidden) sync();});