feat(sync): seamless conversation sync, git-like version control, and offline remote notification

This commit is contained in:
Agent committed 2026-10-21 18:43:29 +00:00
1 parent 8e150dcacd
commit 0f866b1bbd
6 files changed
+471 -17

No files matched your search

+93 -13
View File
@@ -135,15 +135,68 @@ window.__ModuleLoader__.load({
h('rect', { x: 11.5, y: 5.5, width: 3, height: 8, rx: .8 }));
}
function SessionRow({ dev, s, selfId }) {
return h('a', {
className: 'dshr-row', style: { ...S.row, color: 'inherit', textDecoration: 'none' },
href: sessionUrl(dev.device, selfId, s.sessionId),
title: `${s.title || s.sessionId}\n${s.cwd || ''}`,
function SessionRow({ dev, s, selfId, devices = [] }) {
const [busy, setBusy] = useState(false);
const isRemote = dev.device !== selfId;
const doContinue = async (targetDev) => {
setBusy(true);
try {
const r = await post('continue-session', {
sourceDevice: dev.device,
sessionId: s.sessionId,
targetDevice: targetDev,
});
if (r.conflict) {
alert('Notice: Changes diverged! Both instances modified this session while disconnected.');
}
location.href = sessionUrl(targetDev, selfId, r.sessionId);
if (targetDev === selfId) setTimeout(() => location.reload(), 50);
} catch (e) {
alert('Failed to continue session: ' + e.message);
} finally {
setBusy(false);
}
};
return h('div', {
className: 'dshr-row-wrap',
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }
},
s.running ? h('span', { style: S.spin, title: 'running' }) : h('span', { style: { width: 8, flex: 'none' } }),
h('span', { style: S.title }, s.title || h('i', { style: { opacity: .6 } }, 'untitled')),
h('span', { style: S.meta }, ago(s.updatedAt)));
h('a', {
className: 'dshr-row', style: { ...S.row, color: 'inherit', textDecoration: 'none', flex: 1, minWidth: 0 },
href: sessionUrl(dev.device, selfId, s.sessionId),
title: `${s.title || s.sessionId}\n${s.cwd || ''}`,
},
s.running ? h('span', { style: S.spin, title: 'running' }) : h('span', { style: { width: 8, flex: 'none' } }),
h('span', { style: S.title }, s.title || h('i', { style: { opacity: .6 } }, 'untitled')),
s.lineage?.conflict && h('span', { style: { ...S.badge('warn'), marginRight: 6 } }, '⚠ Diverged'),
s.lineage?.status === 'moved' && h('span', { style: { ...S.badge('accent'), marginRight: 6 } }, `⇄ Moved to ${s.lineage.movedTo}`),
h('span', { style: S.meta }, ago(s.updatedAt))
),
h('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0, paddingRight: 6 } },
isRemote && h('button', {
style: { ...S.btn, fontSize: 11, padding: '2px 7px' },
disabled: busy,
title: 'Seamlessly sync conversation to current instance and continue here',
onClick: (e) => { e.preventDefault(); e.stopPropagation(); doContinue(selfId); }
}, busy ? 'Syncing…' : '⇄ Continue here'),
devices.length > 2 && h('select', {
style: { ...S.input, fontSize: 11, padding: '2px 4px', width: 'auto' },
value: '',
disabled: busy,
onChange: (e) => {
const dest = e.target.value;
if (dest) doContinue(dest);
}
},
h('option', { value: '' }, 'Continue on…'),
devices.filter((d) => d.online && d.id !== dev.device).map((d) =>
h('option', { key: d.id, value: d.id }, d.self ? `${d.name} (this device)` : d.name)
)
)
)
);
}
function NewSession({ dev, selfId }) {
@@ -254,7 +307,7 @@ window.__ModuleLoader__.load({
h('span', { style: { display: 'inline-flex', opacity: .8 } }, m.self ? h(FolderIcon, { size: 14 }) : h(ServerIcon, { size: 14 })),
g.title),
g.sessions.length
? g.sessions.map((s) => h(SessionRow, { key: s.sessionId, dev, s, selfId: self?.id }))
? g.sessions.map((s) => h(SessionRow, { key: s.sessionId, dev, s, selfId: self?.id, devices }))
: h('div', { style: { fontSize: 12, opacity: .5, padding: '2px 8px 4px 28px' } }, 'no sessions'))));
}));
}
@@ -313,10 +366,37 @@ window.__ModuleLoader__.load({
h('span', { className: 'dshr-sb-title' }, g.title),
h('button', { className: 'dshr-sb-plus', title: `New session on ${dev.name}`, onClick: (e) => create(e, dev, g) }, '+'),
h('span', { className: 'dshr-sb-dev' }, dev.name || dev.device)),
isOpen && shown.map((s) => h('a', { key: s.sessionId, className: 'dshr-sb-row dshr-sb-sess', role: 'treeitem', href: sessionUrl(dev.device, selfId, s.sessionId), title: `${s.title || s.sessionId}\n${dev.name}` },
s.running ? h('span', { className: 'dshr-sb-run', title: 'running' }) : null,
h('span', { className: 'dshr-sb-title' }, s.title || 'untitled'),
h('span', { className: 'dshr-sb-time' }, ago(s.updatedAt)))),
isOpen && shown.map((s) => h('div', {
key: s.sessionId,
className: 'dshr-sb-row dshr-sb-sess',
role: 'treeitem',
style: { display: 'flex', alignItems: 'center' }
},
h('a', {
style: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0, textDecoration: 'none', color: 'inherit' },
href: sessionUrl(dev.device, selfId, s.sessionId),
title: `${s.title || s.sessionId}\n${dev.name}`,
},
s.running ? h('span', { className: 'dshr-sb-run', title: 'running' }) : null,
h('span', { className: 'dshr-sb-title' }, s.title || 'untitled'),
s.lineage?.conflict && h('span', { style: { fontSize: 9, color: '#f85149', marginRight: 4 } }, '⚠'),
s.lineage?.status === 'moved' && h('span', { style: { fontSize: 9, opacity: 0.7, marginRight: 4 } }, '⇄'),
h('span', { className: 'dshr-sb-time' }, ago(s.updatedAt))),
dev.device !== selfId && h('button', {
className: 'dshr-sb-plus',
title: 'Seamlessly sync conversation to this device',
onClick: async (e) => {
e.stopPropagation(); e.preventDefault();
try {
const r = await post('continue-session', { sourceDevice: dev.device, sessionId: s.sessionId, targetDevice: selfId });
location.href = sessionUrl(selfId, selfId, r.sessionId);
setTimeout(() => location.reload(), 50);
} catch (err) {
alert('Sync error: ' + err.message);
}
}
}, '⇄')
)),
isOpen && !more[g.key] && g.sessions.length > SB_SESSIONS && h('button', { className: 'dshr-sb-more', onClick: () => setMore({ ...more, [g.key]: true }) }, `Show ${g.sessions.length - SB_SESSIONS} more`),
isOpen && !g.sessions.length && h('div', { className: 'dshr-sb-more', style: { cursor: 'default' } }, 'no sessions'));
})));