Ship Assistent 0.8.1: split modules and shared+personal vector memory.
Personal RAG never leaks into the shared store; retrieve merges shared plus the persona chain, with personal overwrite on kind+key. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Swarm Assistent — disk persistence for chats + UI state (Assistent/chats/, Assistent/ui-state.json).
|
||||
* Loaded after assistent.api.js and before assistent.js.
|
||||
*/
|
||||
window.SA = window.SA || {};
|
||||
|
||||
(function () {
|
||||
const LS_CHATS = 'swarm_assistent_chats_v1';
|
||||
const LS_MIGRATED = 'swarm_assistent_chats_on_disk_v1';
|
||||
const SAVE_DEBOUNCE_MS = 700;
|
||||
|
||||
const timers = { chats: new Map(), ui: null };
|
||||
|
||||
function request(name, body) {
|
||||
if (typeof SA.request === 'function') {
|
||||
return SA.request(name, body);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof genericRequest !== 'function') {
|
||||
reject(new Error('genericRequest unavailable'));
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
name,
|
||||
body || {},
|
||||
(data) => (data && data.error ? reject(new Error(String(data.error))) : resolve(data)),
|
||||
0,
|
||||
(err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeChat(raw) {
|
||||
if (!raw || !raw.id) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: String(raw.id),
|
||||
title: String(raw.title || 'Новый чат'),
|
||||
createdAt: Number(raw.createdAt) || Date.now(),
|
||||
updatedAt: Number(raw.updatedAt) || Number(raw.createdAt) || Date.now(),
|
||||
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
||||
params: raw.params && typeof raw.params === 'object' ? raw.params : null,
|
||||
};
|
||||
}
|
||||
|
||||
function readLocalChats() {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(LS_CHATS) || 'null');
|
||||
if (Array.isArray(parsed?.chats)) {
|
||||
return parsed.chats.map(normalizeChat).filter(Boolean);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return [];
|
||||
}
|
||||
|
||||
/** One-shot lift of the browser-only history onto the data volume. */
|
||||
async function migrateLocalChatsToDisk() {
|
||||
if (localStorage.getItem(LS_MIGRATED) === '1') {
|
||||
return [];
|
||||
}
|
||||
const local = readLocalChats().filter((c) => (c.messages || []).length > 0);
|
||||
localStorage.setItem(LS_MIGRATED, '1');
|
||||
if (!local.length) {
|
||||
return [];
|
||||
}
|
||||
for (const chat of local) {
|
||||
try {
|
||||
await saveChat(chat, { immediate: true });
|
||||
} catch (e) {
|
||||
console.warn('Assistent: chat migration failed', chat.id, e);
|
||||
}
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
/** Disk chats, newest first. Falls back to a localStorage migration when the volume is empty. */
|
||||
async function loadChats() {
|
||||
let chats = [];
|
||||
try {
|
||||
const data = await request('AssistentListChats', { with_messages: true });
|
||||
chats = (data?.chats || []).map(normalizeChat).filter(Boolean);
|
||||
} catch (e) {
|
||||
console.warn('Assistent: disk chats unavailable', e);
|
||||
return null;
|
||||
}
|
||||
if (!chats.length) {
|
||||
const migrated = await migrateLocalChatsToDisk();
|
||||
if (migrated.length) {
|
||||
chats = migrated;
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem(LS_MIGRATED, '1');
|
||||
}
|
||||
return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||||
}
|
||||
|
||||
async function getChat(id) {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
const data = await request('AssistentGetChat', { id });
|
||||
return normalizeChat(data?.chat);
|
||||
}
|
||||
|
||||
function saveChat(chat, { immediate = false } = {}) {
|
||||
const clean = normalizeChat(chat);
|
||||
if (!clean) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const send = () => {
|
||||
timers.chats.delete(clean.id);
|
||||
return request('AssistentSaveChat', {
|
||||
id: clean.id,
|
||||
title: clean.title,
|
||||
messages: clean.messages,
|
||||
params: clean.params,
|
||||
createdAt: clean.createdAt,
|
||||
updatedAt: clean.updatedAt,
|
||||
});
|
||||
};
|
||||
if (immediate) {
|
||||
const pending = timers.chats.get(clean.id);
|
||||
if (pending) {
|
||||
clearTimeout(pending);
|
||||
}
|
||||
return send();
|
||||
}
|
||||
const pending = timers.chats.get(clean.id);
|
||||
if (pending) {
|
||||
clearTimeout(pending);
|
||||
}
|
||||
timers.chats.set(clean.id, setTimeout(() => {
|
||||
send().catch((e) => console.warn('Assistent: chat save failed', clean.id, e));
|
||||
}, SAVE_DEBOUNCE_MS));
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
function deleteChat(id) {
|
||||
if (!id) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const pending = timers.chats.get(id);
|
||||
if (pending) {
|
||||
clearTimeout(pending);
|
||||
timers.chats.delete(id);
|
||||
}
|
||||
return request('AssistentDeleteChat', { id });
|
||||
}
|
||||
|
||||
async function loadUiState() {
|
||||
try {
|
||||
const data = await request('AssistentGetUiState', {});
|
||||
const ui = data?.ui_state;
|
||||
return ui && typeof ui === 'object' ? ui : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveUiState(uiState, { immediate = false } = {}) {
|
||||
if (!uiState || typeof uiState !== 'object') {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const send = () => {
|
||||
timers.ui = null;
|
||||
return request('AssistentSaveUiState', { ui_state: uiState });
|
||||
};
|
||||
if (timers.ui) {
|
||||
clearTimeout(timers.ui);
|
||||
timers.ui = null;
|
||||
}
|
||||
if (immediate) {
|
||||
return send();
|
||||
}
|
||||
timers.ui = setTimeout(() => {
|
||||
send().catch((e) => console.warn('Assistent: ui-state save failed', e));
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
SA.persist = {
|
||||
LS_CHATS,
|
||||
loadChats,
|
||||
getChat,
|
||||
saveChat,
|
||||
deleteChat,
|
||||
loadUiState,
|
||||
saveUiState,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user