From 9791b29acebdfe9a49c0aa7e19972233bd29b383 Mon Sep 17 00:00:00 2001 From: "planetrenox@protonmail.com" Date: Tue, 20 Oct 2026 13:40:46 +0000 Subject: [PATCH] Open a popover from the thread Sync button instead of a confirm prompt Thread Sync now shows Upload to GitHub / Download from GitHub in a popover, like sunes sync, so it can be dismissed instead of forcing a choice. It closes on an outside click, resize, the sidebar overlay, or picking an option. The push and pull code is unchanged, and the readme's sync steps now name the popover options. Co-Authored-By: Claude Opus 5.5 --- readme.md | 4 ++-- src/dom.js | 3 ++- src/main.js | 12 +++++++++--- src/parts/modals.html | 4 ++++ src/parts/sidebars.html | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index b5fd056..7c7e022 100644 --- a/readme.md +++ b/readme.md @@ -59,7 +59,7 @@ Never lose a conversation again. Sune can sync all your threads to a GitHub repo 5. **Sync** - Hit the **Sync** button after starting a new chat in there. - - **OK** = Push your local threads up to GitHub - - **Cancel** = Pull threads down from GitHub + - **Upload to GitHub** = Push your local threads up to GitHub + - **Download from GitHub** = Pull threads down from GitHub That's it. Your threads are now backed up as JSON files in your repo. You can sync across devices, never lose a chat, and even browse your conversations directly on GitHub. diff --git a/src/dom.js b/src/dom.js index c04643d..cd5d3a9 100644 --- a/src/dom.js +++ b/src/dom.js @@ -20,6 +20,7 @@ export const el = window.el = Object.fromEntries( 'importAccountSettingsInput','accountTabUser','accountPanelUser','set_user_name', 'userAvatarPreview','setUserAvatarBtn','userAvatarInput','threadRepoInput','threadBackBtn', 'threadFolderBtn','threadSyncBtn','suneRepoInput','suneSyncBtn', - 'suneSyncBadge','sunesSyncPopover','sunesSyncUploadBtn','sunesSyncDownloadBtn' + 'suneSyncBadge','sunesSyncPopover','sunesSyncUploadBtn','sunesSyncDownloadBtn', + 'threadsSyncPopover','threadsSyncUploadBtn','threadsSyncDownloadBtn' ].map(id => [id, document.getElementById(id)]) ); diff --git a/src/main.js b/src/main.js index 2662359..51a69b4 100644 --- a/src/main.js +++ b/src/main.js @@ -101,6 +101,8 @@ function showSunePopover(btn,id){menuSuneId=id;el.sunePopover.classList.remove(' const hideSunesSyncPopover=()=>{el.sunesSyncPopover.classList.add('hidden')},hideSuneSyncPopover=hideSunesSyncPopover; function showSunesSyncPopover(btn){el.sunesSyncPopover.classList.remove('hidden');positionPopover(btn||el.suneSyncBtn,el.sunesSyncPopover);icons()} const showSuneSyncPopover=showSunesSyncPopover; +const hideThreadsSyncPopover=()=>{el.threadsSyncPopover.classList.add('hidden')} +function showThreadsSyncPopover(){el.threadsSyncPopover.classList.remove('hidden');positionPopover(el.threadSyncBtn,el.threadsSyncPopover);icons()} $(el.threadList).on('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread'),type=openBtn.getAttribute('data-type');if(type==='file'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);window.open(`https://github.com/${info.owner}/${info.repo}/blob/${info.branch}/${id}`,'_blank')}return}if(type==='folder'){const u=el.threadRepoInput.value.trim();el.threadRepoInput.value=u+(u.endsWith('/')?'':'/')+id;el.threadRepoInput.dispatchEvent(new Event('change'));return}const th=THREAD.get(id);if(!th)return;if(id===state.currentThreadId){el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}state.currentThreadId=id;clearChat();const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';let msgs=await localforage.getItem(prefix+id);if((!msgs||!Array.isArray(msgs)||!msgs.length)&&u.startsWith('gh://')){try{const info=parseGhUrl(u),fileName=serializeThreadName(th),text=await ghGetFileContent(info,fileName);if(text){try{msgs=JSON.parse(text);await localforage.setItem(prefix+id,msgs);th.status='synced';await THREAD.save()}catch(pe){console.error('[Sune] Thread JSON parse failed for',fileName,'len',text.length,pe)}}else{console.warn('[Sune] Remote thread returned no content:',fileName)}}catch(e){console.error('[Sune] Remote fetch failed',e)}}if(state.currentThreadId!==id)return;state.messages=Array.isArray(msgs)?[...msgs]:[];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m))}adoptThread();await renderSuneHTML();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}if(menuBtn){e.stopPropagation();showThreadPopover(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}}) $(el.threadList).on('scroll',()=>{ if(isAddingThreads||el.threadList.scrollTop+el.threadList.clientHeight { }; async function init(){gcStorage();const u=localStorage.getItem('thread_repo_url')||'',suR=localStorage.getItem('sune_repo_url')||'';el.threadRepoInput.value=u;el.suneRepoInput.value=suR;el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);await THREAD.load();await renderThreads();await Promise.allSettled(STICKY_SUNES.map(s=>SUNE.fetchDotSune(s)));renderSidebar();renderUserUI();await reflectActiveSune();if(suR.startsWith('gh://'))checkSuneSyncStatus();clearChat();icons();kbBind();kbUpdate();restoreLive();localforage.dropInstance({name:'threads_cache'}).catch(()=>{})} -$(window).on('resize',()=>{hideThreadPopover();hideSunePopover();hideSunesSyncPopover()}) +$(window).on('resize',()=>{hideThreadPopover();hideSunePopover();hideSunesSyncPopover();hideThreadsSyncPopover()}) $(document).on('click',e=>{if(el.sunesSyncPopover&&!el.sunesSyncPopover.classList.contains('hidden')&&!el.sunesSyncPopover.contains(e.target)&&!el.suneSyncBtn.contains(e.target))hideSunesSyncPopover()}); +$(document).on('click',e=>{if(!el.threadsSyncPopover.classList.contains('hidden')&&!el.threadsSyncPopover.contains(e.target)&&!el.threadSyncBtn.contains(e.target))hideThreadsSyncPopover()}); const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']};function showHtmlTab(key){Object.entries(htmlTabs).forEach(([k,[tb,pn]])=>{const a=k===key;el[tb].classList.toggle('border-black',a);el[tb].classList.toggle('border-transparent',!a);el[tb].classList.toggle('hover:border-gray-300',!a);el[pn].classList.toggle('hidden',!a)})} el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html'; el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension'); @@ -236,7 +239,10 @@ const pullThreads=async(invalidateAll=false)=>{ $(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.trim();localStorage.setItem('thread_repo_url',u);if(state.currentThreadId){state.currentThreadId=null;clearChat()}el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);if(u.startsWith('gh://'))await pullThreads();else{await THREAD.load();await renderThreads()}}); $(el.threadBackBtn).on('click',()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const p=u.split('/');if(p.length>3){p.pop();el.threadRepoInput.value=p.join('/');el.threadRepoInput.dispatchEvent(new Event('change'))}}); $(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)return;THREAD.list.unshift({id:n.trim(),title:n.trim(),type:'folder',updatedAt:Date.now()});await THREAD.save();await renderThreads()}); -$(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t);let msgs=await localforage.getItem('rem_t_'+t.id);if((!msgs||!Array.isArray(msgs))&&remoteMap[t.id]){const text=await ghGetFileContent(info,remoteMap[t.id].name);if(text){try{msgs=JSON.parse(text);await localforage.setItem('rem_t_'+t.id,msgs)}catch(e){console.error(e)}}}if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const x=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs||[],null,2)),branch:info.branch,sha:x?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else if(await pullThreads(true)){state.currentThreadId=null;clearChat();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}}); +const syncThreads=async push=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const info=parseGhUrl(u);try{if(push){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t);let msgs=await localforage.getItem('rem_t_'+t.id);if((!msgs||!Array.isArray(msgs))&&remoteMap[t.id]){const text=await ghGetFileContent(info,remoteMap[t.id].name);if(text){try{msgs=JSON.parse(text);await localforage.setItem('rem_t_'+t.id,msgs)}catch(e){console.error(e)}}}if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const x=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs||[],null,2)),branch:info.branch,sha:x?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else if(await pullThreads(true)){state.currentThreadId=null;clearChat();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}}; +$(el.threadSyncBtn).on('click',e=>{e.stopPropagation();if(el.threadRepoInput.value.trim().startsWith('gh://'))showThreadsSyncPopover()}); +$(el.threadsSyncUploadBtn).on('click',()=>{hideThreadsSyncPopover();syncThreads(true)}); +$(el.threadsSyncDownloadBtn).on('click',()=>{hideThreadsSyncPopover();syncThreads(false)}); let suneSyncBusy=false; const checkSuneSyncStatus=async()=>{ @@ -381,4 +387,4 @@ const getActiveJar=()=>!el.htmlEditor.classList.contains('hidden')?jars.html:jar $(el.copyHTML).on('click',async()=>{const jar=getActiveJar();await copyToClipboard(jar?jar.toString():'')}) $(el.pasteHTML).on('click',async()=>{try{const t=await navigator.clipboard.readText();const jar=getActiveJar();if(jar)jar.updateCode(t)}catch{}}) -Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,renderUserUI,enhanceCodeBlocks,getSuneLabel,_createMessageRow,msgRow,partsToText,copyToClipboard,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,serializeThreadName,deserializeThreadName,ensureThreadOnFirstUser,generateTitleWithAI,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,hideSuneSyncPopover,showSuneSyncPopover,hideSunesSyncPopover,showSunesSyncPopover,setSuneSyncStatus,checkSuneSyncStatus,performSuneDownload,performSuneUpload,updateAttachBadge,toAttach,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,showAccountTab,openAccountSettings,closeAccountSettings,getBubbleById,getActiveJar,imgToWebp,ghApi,parseGhUrl,ghGetFileContent,pullThreads,suneStorage,gcStorage,cleanSuneStorage}); +Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,renderUserUI,enhanceCodeBlocks,getSuneLabel,_createMessageRow,msgRow,partsToText,copyToClipboard,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,serializeThreadName,deserializeThreadName,ensureThreadOnFirstUser,generateTitleWithAI,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,hideSuneSyncPopover,showSuneSyncPopover,hideSunesSyncPopover,showSunesSyncPopover,hideThreadsSyncPopover,showThreadsSyncPopover,setSuneSyncStatus,checkSuneSyncStatus,performSuneDownload,performSuneUpload,updateAttachBadge,toAttach,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,showAccountTab,openAccountSettings,closeAccountSettings,getBubbleById,getActiveJar,imgToWebp,ghApi,parseGhUrl,ghGetFileContent,pullThreads,suneStorage,gcStorage,cleanSuneStorage}); diff --git a/src/parts/modals.html b/src/parts/modals.html index b94167e..dd70800 100644 --- a/src/parts/modals.html +++ b/src/parts/modals.html @@ -17,6 +17,10 @@ + diff --git a/src/parts/sidebars.html b/src/parts/sidebars.html index d82933c..de28729 100644 --- a/src/parts/sidebars.html +++ b/src/parts/sidebars.html @@ -1,4 +1,4 @@ - +