Memory page and search inside chats, files and memory (Ctrl+K, GET /search)
ci / ui (push) Waiting to run
ci / check (push) Failing after 7s
ci / start (push) Failing after 5s

The account's memory as the web's Settings -> Memory keeps it: add, change,
remove, over /api/v2/memories. Ctrl+K asks the server's search once two
letters are typed and lists the hits below the titles, the matched ranges
drawn as <mark> elements over plain text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 13:44:49 +00:00
1 parent be70e1a628
commit d691a4dcfa
9 files changed
+515 -11

No files matched your search

+27
View File
@@ -1205,6 +1205,11 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.palette__icon { display: grid; place-items: center; color: var(--ink-muted); }
.palette__label { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.palette__hint { font-size: var(--text-xs); color: var(--ink-faint); white-space: nowrap; }
/* A hit found inside: its title over the snippet, the words that matched marked. */
.palette__label--two { display: grid; padding: var(--sp-1) 0; white-space: normal; }
.palette__title, .palette__snippet { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.palette__snippet { font-size: var(--text-xs); color: var(--ink-muted); }
.palette__snippet mark { color: var(--ink); background: var(--surface-active); border-radius: var(--radius-sm); }
.palette__none { padding: var(--sp-3); font-size: var(--text-sm); color: var(--ink-muted); }
@@ -1691,6 +1696,28 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.arch__page { display: grid; gap: var(--sp-3); }
.head__unarchive { white-space: nowrap; }
/* --- Memory: a new one on top, then each record over its group and date · Edit · more. */
.mnew { display: grid; gap: var(--sp-2); }
.mnew textarea { width: 100%; }
.mnew__foot { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-2); align-items: center; }
.mnew__foot select { justify-self: start; max-width: 100%; }
.mrow {
display: grid;
grid-template-columns: minmax(0, 1fr) auto var(--control-h-sm);
gap: var(--sp-3);
align-items: center;
min-height: var(--control-h);
padding: var(--sp-2) 0;
border-bottom: var(--border-w) solid var(--border);
}
.mrow.is-editing { grid-template-columns: minmax(0, 1fr); }
.mrow.is-editing textarea { width: 100%; }
.mrow.is-marked { background: var(--surface-active); border-radius: var(--radius-lg); padding-inline: var(--sp-2); }
.mrow__words { display: grid; gap: var(--sp-1); min-width: 0; }
.mrow__text { margin: 0; font: var(--text-sm) / var(--leading-normal) var(--font-body); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
.mrow__sub { margin: 0; font: var(--text-xs) var(--font-ui); color: var(--ink-faint); }
/* --- Narrower windows ---------------------------------------------------------------------- */
/* 64rem (--bp-wide): a panel goes over the chat instead of beside it. */
+64
View File
@@ -44,6 +44,7 @@ const LEMBAS_I18N = {
"About": "O aplikaci",
"Account": "Účet",
"Activity": "Aktivita",
"Add": "Přidat",
"Add folder…": "Přidat složku…",
"Address of your LLeMbas server": "Adresa vašeho serveru LLeMbas",
"Agent": "Agent",
@@ -105,6 +106,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "Spočítal model při poslední odpovědi; kategorie jsou odhadnuté.",
"Create": "Vytvořit",
"Cut short here; the whole of it is in the web interface.": "Zde zkráceno; celé je to ve webovém rozhraní.",
"Data group": "Datová skupina",
"Days": "Dny",
"Delete": "Smazat",
"Delete chat": "Smazat konverzaci",
@@ -167,6 +169,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — nejdřív se zeptá",
"Max": "Max",
"Maximise": "Maximalizovat",
"Memory": "Paměť",
"Memory and personal": "Paměť a osobní",
"Menu": "Nabídka",
"Message": "Zpráva",
@@ -201,10 +204,12 @@ const LEMBAS_I18N = {
"Not measured.": "Neměřeno.",
"Not run yet": "Ještě neběžela",
"Not yet": "Ještě ne",
"Noted by a model": "Zaznamenal model",
"Nothing has been run in this shell yet.": "V tomto shellu se zatím nic nespustilo.",
"Nothing has been said in the room yet.": "V místnosti zatím nikdo nic neřekl.",
"Nothing is scheduled.": "Nic není naplánováno.",
"Nothing matches.": "Nic neodpovídá.",
"Nothing remembered yet.": "Zatím si nic nepamatuje.",
"Nothing running.": "Nic neběží.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nic neběží. Podagenti, členové týmu a příkazy na pozadí se tu zobrazí, dokud pracují.",
"Nothing to report": "Nic k hlášení",
@@ -245,6 +250,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Vygenerovat odpověď znovu",
"Remove": "Odebrat",
"Remove %(dir)s": "Odebrat %(dir)s",
"Remove this memory?": "Odstranit tento záznam z paměti?",
"Rename": "Přejmenovat",
"Rename chat": "Přejmenovat konverzaci",
"Rename folder": "Přejmenovat složku",
@@ -288,6 +294,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlášením se přihlášení aplikace na serveru zruší; propojení agenta zůstane. Chcete-li použít jiný server, odhlaste se.",
"Since the last reply": "Od poslední odpovědi",
"Skipped": "Vynecháno",
"Something to remember…": "Něco k zapamatování…",
"Source": "Zdroj",
"Start the agent": "Spustit agenta",
"Stop": "Zastavit",
@@ -382,6 +389,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Čeká se na {{model}}: {{reason}}",
"What it does": "Co dělá",
"What should happen, and when": "Co se má stát a kdy",
"What your models remember about you, across chats. Change or remove anything here.": "Co si o vás vaše modely pamatují napříč konverzacemi. Cokoli tu můžete změnit nebo odstranit.",
"When": "Kdy",
"When it matters": "Když na tom záleží",
"Writing": "Píše",
@@ -476,6 +484,7 @@ const LEMBAS_I18N = {
"About": "Über",
"Account": "Konto",
"Activity": "Aktivität",
"Add": "Hinzufügen",
"Add folder…": "Ordner hinzufügen…",
"Address of your LLeMbas server": "Adresse Ihres LLeMbas-Servers",
"Agent": "Agent",
@@ -537,6 +546,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "Vom Modell bei der letzten Antwort gezählt; die Kategorien sind geschätzt.",
"Create": "Erstellen",
"Cut short here; the whole of it is in the web interface.": "Hier gekürzt; vollständig steht es in der Weboberfläche.",
"Data group": "Datengruppe",
"Days": "Tage",
"Delete": "Löschen",
"Delete chat": "Chat löschen",
@@ -599,6 +609,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — fragt zuerst",
"Max": "Max",
"Maximise": "Maximieren",
"Memory": "Gedächtnis",
"Memory and personal": "Gedächtnis und Persönliches",
"Menu": "Menü",
"Message": "Nachricht",
@@ -633,10 +644,12 @@ const LEMBAS_I18N = {
"Not measured.": "Nicht gemessen.",
"Not run yet": "Noch nicht gelaufen",
"Not yet": "Noch nicht",
"Noted by a model": "Von einem Modell notiert",
"Nothing has been run in this shell yet.": "In dieser Shell wurde noch nichts ausgeführt.",
"Nothing has been said in the room yet.": "Im Raum wurde noch nichts gesagt.",
"Nothing is scheduled.": "Nichts ist geplant.",
"Nothing matches.": "Nichts gefunden.",
"Nothing remembered yet.": "Noch nichts behalten.",
"Nothing running.": "Nichts läuft.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Im Hintergrund läuft nichts. Subagenten, die Mitglieder eines Teams und Hintergrundbefehle erscheinen hier, solange sie arbeiten.",
"Nothing to report": "Nichts zu melden",
@@ -677,6 +690,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Antwort neu generieren",
"Remove": "Entfernen",
"Remove %(dir)s": "%(dir)s entfernen",
"Remove this memory?": "Diese Erinnerung entfernen?",
"Rename": "Umbenennen",
"Rename chat": "Chat umbenennen",
"Rename folder": "Ordner umbenennen",
@@ -720,6 +734,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Beim Abmelden gibt die App ihre Anmeldung am Server auf; die Verbindung des Agenten bleibt. Melden Sie sich ab, um einen anderen Server zu verwenden.",
"Since the last reply": "Seit der letzten Antwort",
"Skipped": "Ausgelassen",
"Something to remember…": "Etwas, das behalten werden soll…",
"Source": "Quelltext",
"Start the agent": "Agent starten",
"Stop": "Stoppen",
@@ -814,6 +829,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Warte auf {{model}}: {{reason}}",
"What it does": "Was sie tut",
"What should happen, and when": "Was soll wann geschehen?",
"What your models remember about you, across chats. Change or remove anything here.": "Was Ihre Modelle über Sie behalten, über alle Chats hinweg. Hier können Sie alles ändern oder entfernen.",
"When": "Wann",
"When it matters": "Wenn es zählt",
"Writing": "Schreibe",
@@ -908,6 +924,7 @@ const LEMBAS_I18N = {
"About": "Acerca de",
"Account": "Cuenta",
"Activity": "Actividad",
"Add": "Añadir",
"Add folder…": "Añadir carpeta…",
"Address of your LLeMbas server": "Dirección de su servidor LLeMbas",
"Agent": "Agente",
@@ -969,6 +986,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "Contado por el modelo en la última respuesta; las categorías son estimaciones.",
"Create": "Crear",
"Cut short here; the whole of it is in the web interface.": "Recortado aquí; completo está en la interfaz web.",
"Data group": "Grupo de datos",
"Days": "Días",
"Delete": "Eliminar",
"Delete chat": "Eliminar chat",
@@ -1031,6 +1049,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — pregunta primero",
"Max": "Máx",
"Maximise": "Maximizar",
"Memory": "Memoria",
"Memory and personal": "Memoria y datos personales",
"Menu": "Menú",
"Message": "Mensaje",
@@ -1065,10 +1084,12 @@ const LEMBAS_I18N = {
"Not measured.": "Sin medir.",
"Not run yet": "Aún no se ha ejecutado",
"Not yet": "Todavía no",
"Noted by a model": "Anotado por un modelo",
"Nothing has been run in this shell yet.": "Aún no se ha ejecutado nada en este shell.",
"Nothing has been said in the room yet.": "Todavía no se ha dicho nada en la sala.",
"Nothing is scheduled.": "No hay nada programado.",
"Nothing matches.": "No hay coincidencias.",
"Nothing remembered yet.": "Todavía no se recuerda nada.",
"Nothing running.": "Nada en marcha.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "No hay nada en segundo plano. Los subagentes, los miembros de un equipo y los comandos en segundo plano aparecen aquí mientras trabajan.",
"Nothing to report": "Nada que informar",
@@ -1109,6 +1130,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Regenerar respuesta",
"Remove": "Quitar",
"Remove %(dir)s": "Quitar %(dir)s",
"Remove this memory?": "¿Quitar este recuerdo?",
"Rename": "Renombrar",
"Rename chat": "Renombrar chat",
"Rename folder": "Renombrar carpeta",
@@ -1152,6 +1174,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Al cerrar sesión, la aplicación renuncia a su acceso en el servidor; la vinculación del agente se mantiene. Cierre sesión para usar otro servidor.",
"Since the last reply": "Desde la última respuesta",
"Skipped": "Omitida",
"Something to remember…": "Algo que recordar…",
"Source": "Código fuente",
"Start the agent": "Iniciar el agente",
"Stop": "Detener",
@@ -1246,6 +1269,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Esperando a {{model}}: {{reason}}",
"What it does": "Qué hace",
"What should happen, and when": "Qué debe ocurrir, y cuándo",
"What your models remember about you, across chats. Change or remove anything here.": "Lo que sus modelos recuerdan de usted entre chats. Aquí puede cambiar o quitar cualquier cosa.",
"When": "Cuándo",
"When it matters": "Cuando importa",
"Writing": "Escribiendo",
@@ -1340,6 +1364,7 @@ const LEMBAS_I18N = {
"About": "À propos",
"Account": "Compte",
"Activity": "Activité",
"Add": "Ajouter",
"Add folder…": "Ajouter un dossier…",
"Address of your LLeMbas server": "Adresse de votre serveur LLeMbas",
"Agent": "Agent",
@@ -1401,6 +1426,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "Compté par le modèle lors de la dernière réponse ; les catégories sont estimées.",
"Create": "Créer",
"Cut short here; the whole of it is in the web interface.": "Tronqué ici ; le tout est dans l'interface web.",
"Data group": "Groupe de données",
"Days": "Jours",
"Delete": "Supprimer",
"Delete chat": "Supprimer la conversation",
@@ -1463,6 +1489,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — demande d'abord",
"Max": "Max",
"Maximise": "Agrandir",
"Memory": "Mémoire",
"Memory and personal": "Mémoire et informations personnelles",
"Menu": "Menu",
"Message": "Message",
@@ -1497,10 +1524,12 @@ const LEMBAS_I18N = {
"Not measured.": "Non mesuré.",
"Not run yet": "Pas encore exécutée",
"Not yet": "Pas encore",
"Noted by a model": "Noté par un modèle",
"Nothing has been run in this shell yet.": "Rien n'a encore été exécuté dans ce shell.",
"Nothing has been said in the room yet.": "Rien n’a encore été dit dans la salle.",
"Nothing is scheduled.": "Rien n’est planifié.",
"Nothing matches.": "Aucun résultat.",
"Nothing remembered yet.": "Rien n’est encore retenu.",
"Nothing running.": "Rien en cours.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Rien ne tourne en arrière-plan. Les sous-agents, les membres d’une équipe et les commandes en arrière-plan s’affichent ici pendant qu’ils travaillent.",
"Nothing to report": "Rien à signaler",
@@ -1541,6 +1570,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Régénérer la réponse",
"Remove": "Retirer",
"Remove %(dir)s": "Retirer %(dir)s",
"Remove this memory?": "Supprimer ce souvenir ?",
"Rename": "Renommer",
"Rename chat": "Renommer la conversation",
"Rename folder": "Renommer le dossier",
@@ -1584,6 +1614,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "La déconnexion abandonne la connexion de l'application au serveur ; la liaison de l'agent reste. Déconnectez-vous pour utiliser un autre serveur.",
"Since the last reply": "Depuis la dernière réponse",
"Skipped": "Ignorée",
"Something to remember…": "Quelque chose à retenir…",
"Source": "Source",
"Start the agent": "Démarrer l'agent",
"Stop": "Arrêter",
@@ -1678,6 +1709,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "En attente de {{model}} : {{reason}}",
"What it does": "Ce qu’elle fait",
"What should happen, and when": "Ce qui doit se passer, et quand",
"What your models remember about you, across chats. Change or remove anything here.": "Ce que vos modèles retiennent de vous, d’une conversation à l’autre. Modifiez ou supprimez ce que vous voulez ici.",
"When": "Quand",
"When it matters": "Quand c'est important",
"Writing": "Écriture",
@@ -1772,6 +1804,7 @@ const LEMBAS_I18N = {
"About": "Névjegy",
"Account": "Fiók",
"Activity": "Tevékenység",
"Add": "Hozzáadás",
"Add folder…": "Mappa hozzáadása…",
"Address of your LLeMbas server": "A LLeMbas-kiszolgáló címe",
"Agent": "Ügynök",
@@ -1833,6 +1866,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "A modell számolta az utolsó válasznál; a kategóriák becslések.",
"Create": "Létrehozás",
"Cut short here; the whole of it is in the web interface.": "Itt rövidítve; a teljes a webes felületen van.",
"Data group": "Adatcsoport",
"Days": "Napok",
"Delete": "Törlés",
"Delete chat": "Csevegés törlése",
@@ -1895,6 +1929,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — előbb kérdez",
"Max": "Max",
"Maximise": "Teljes méret",
"Memory": "Memória",
"Memory and personal": "Memória és személyes adatok",
"Menu": "Menü",
"Message": "Üzenet",
@@ -1929,10 +1964,12 @@ const LEMBAS_I18N = {
"Not measured.": "Nincs mérve.",
"Not run yet": "Még nem futott",
"Not yet": "Még nem",
"Noted by a model": "Egy modell jegyezte fel",
"Nothing has been run in this shell yet.": "Ebben a shellben még semmi sem futott.",
"Nothing has been said in the room yet.": "A szobában még senki nem mondott semmit.",
"Nothing is scheduled.": "Nincs semmi ütemezve.",
"Nothing matches.": "Nincs találat.",
"Nothing remembered yet.": "Még nincs megjegyezve semmi.",
"Nothing running.": "Semmi sem fut.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "A háttérben semmi sem fut. Az alügynökök, egy csapat tagjai és a háttérparancsok itt jelennek meg, amíg dolgoznak.",
"Nothing to report": "Nincs jelentenivaló",
@@ -1973,6 +2010,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Válasz újragenerálása",
"Remove": "Eltávolítás",
"Remove %(dir)s": "%(dir)s eltávolítása",
"Remove this memory?": "Eltávolítja ezt az emléket?",
"Rename": "Átnevezés",
"Rename chat": "Csevegés átnevezése",
"Rename folder": "Mappa átnevezése",
@@ -2016,6 +2054,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Kijelentkezéskor az alkalmazás lemond a szerveren lévő bejelentkezéséről; az ügynök kapcsolata megmarad. Más szerver használatához jelentkezzen ki.",
"Since the last reply": "Az utolsó válasz óta",
"Skipped": "Kihagyva",
"Something to remember…": "Valami, amit meg kell jegyezni…",
"Source": "Forrás",
"Start the agent": "Ügynök indítása",
"Stop": "Leállítás",
@@ -2110,6 +2149,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Várakozás a(z) {{model}} modellre: {{reason}}",
"What it does": "Mit csinál",
"What should happen, and when": "Mi történjen, és mikor",
"What your models remember about you, across chats. Change or remove anything here.": "Amit a modelljei megjegyeznek Önről a beszélgetéseken át. Itt bármit módosíthat vagy eltávolíthat.",
"When": "Mikor",
"When it matters": "Ha számít",
"Writing": "Írás",
@@ -2204,6 +2244,7 @@ const LEMBAS_I18N = {
"About": "Informazioni",
"Account": "Account",
"Activity": "Attività",
"Add": "Aggiungi",
"Add folder…": "Aggiungi cartella…",
"Address of your LLeMbas server": "Indirizzo del tuo server LLeMbas",
"Agent": "Agente",
@@ -2265,6 +2306,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "Contato dal modello all’ultima risposta; le categorie sono stime.",
"Create": "Crea",
"Cut short here; the whole of it is in the web interface.": "Troncato qui; il resto è nell'interfaccia web.",
"Data group": "Gruppo di dati",
"Days": "Giorni",
"Delete": "Elimina",
"Delete chat": "Elimina chat",
@@ -2327,6 +2369,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — chiede prima",
"Max": "Max",
"Maximise": "Ingrandisci",
"Memory": "Memoria",
"Memory and personal": "Memoria e dati personali",
"Menu": "Menu",
"Message": "Messaggio",
@@ -2361,10 +2404,12 @@ const LEMBAS_I18N = {
"Not measured.": "Non misurato.",
"Not run yet": "Non ancora eseguita",
"Not yet": "Non ancora",
"Noted by a model": "Annotato da un modello",
"Nothing has been run in this shell yet.": "In questa shell non è ancora stato eseguito nulla.",
"Nothing has been said in the room yet.": "Nella stanza non è ancora stato detto nulla.",
"Nothing is scheduled.": "Non c’è niente di pianificato.",
"Nothing matches.": "Nessun risultato.",
"Nothing remembered yet.": "Ancora niente da ricordare.",
"Nothing running.": "Nulla in esecuzione.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Nulla è in esecuzione in background. Sottoagenti, membri di una squadra e comandi in background compaiono qui mentre lavorano.",
"Nothing to report": "Niente da segnalare",
@@ -2405,6 +2450,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Rigenera risposta",
"Remove": "Rimuovi",
"Remove %(dir)s": "Rimuovi %(dir)s",
"Remove this memory?": "Rimuovere questo ricordo?",
"Rename": "Rinomina",
"Rename chat": "Rinomina chat",
"Rename folder": "Rinomina cartella",
@@ -2448,6 +2494,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Uscendo, l'app rinuncia al suo accesso sul server; il collegamento dell'agente resta. Esca per usare un altro server.",
"Since the last reply": "Dall’ultima risposta",
"Skipped": "Saltata",
"Something to remember…": "Qualcosa da ricordare…",
"Source": "Sorgente",
"Start the agent": "Avvia l'agente",
"Stop": "Ferma",
@@ -2542,6 +2589,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "In attesa di {{model}}: {{reason}}",
"What it does": "Cosa fa",
"What should happen, and when": "Cosa deve succedere, e quando",
"What your models remember about you, across chats. Change or remove anything here.": "Ciò che i tuoi modelli ricordano di te tra una chat e l’altra. Qui puoi cambiare o rimuovere qualsiasi cosa.",
"When": "Quando",
"When it matters": "Quando conta",
"Writing": "Scrittura",
@@ -2636,6 +2684,7 @@ const LEMBAS_I18N = {
"About": "O aplikacji",
"Account": "Konto",
"Activity": "Aktywność",
"Add": "Dodaj",
"Add folder…": "Dodaj folder…",
"Address of your LLeMbas server": "Adres Twojego serwera LLeMbas",
"Agent": "Agent",
@@ -2697,6 +2746,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "Policzone przez model przy ostatniej odpowiedzi; kategorie są szacunkowe.",
"Create": "Utwórz",
"Cut short here; the whole of it is in the web interface.": "Tu skrócone; całość jest w interfejsie WWW.",
"Data group": "Grupa danych",
"Days": "Dni",
"Delete": "Usuń",
"Delete chat": "Usuń czat",
@@ -2759,6 +2809,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — najpierw pyta",
"Max": "Max",
"Maximise": "Maksymalizuj",
"Memory": "Pamięć",
"Memory and personal": "Pamięć i dane osobiste",
"Menu": "Menu",
"Message": "Wiadomość",
@@ -2793,10 +2844,12 @@ const LEMBAS_I18N = {
"Not measured.": "Nie zmierzono.",
"Not run yet": "Jeszcze nie uruchomione",
"Not yet": "Jeszcze nie",
"Noted by a model": "Zanotowane przez model",
"Nothing has been run in this shell yet.": "W tej powłoce nic jeszcze nie uruchomiono.",
"Nothing has been said in the room yet.": "W pokoju nikt jeszcze nic nie powiedział.",
"Nothing is scheduled.": "Nic nie jest zaplanowane.",
"Nothing matches.": "Nic nie pasuje.",
"Nothing remembered yet.": "Na razie nic nie zapamiętano.",
"Nothing running.": "Nic nie działa.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "W tle nic nie działa. Podagenci, członkowie zespołu i polecenia w tle pojawiają się tu, gdy pracują.",
"Nothing to report": "Nic do zgłoszenia",
@@ -2837,6 +2890,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Wygeneruj odpowiedź ponownie",
"Remove": "Usuń",
"Remove %(dir)s": "Usuń %(dir)s",
"Remove this memory?": "Usunąć to wspomnienie?",
"Rename": "Zmień nazwę",
"Rename chat": "Zmień nazwę czatu",
"Rename folder": "Zmień nazwę folderu",
@@ -2880,6 +2934,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Wylogowanie zrzeka się logowania aplikacji na serwerze; połączenie agenta zostaje. Wyloguj się, aby użyć innego serwera.",
"Since the last reply": "Od ostatniej odpowiedzi",
"Skipped": "Pominięty",
"Something to remember…": "Coś do zapamiętania…",
"Source": "Źródło",
"Start the agent": "Uruchom agenta",
"Stop": "Zatrzymać",
@@ -2974,6 +3029,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Czekam na {{model}}: {{reason}}",
"What it does": "Co robi",
"What should happen, and when": "Co ma się stać i kiedy",
"What your models remember about you, across chats. Change or remove anything here.": "Co Twoje modele pamiętają o Tobie we wszystkich czatach. Tutaj możesz wszystko zmienić lub usunąć.",
"When": "Kiedy",
"When it matters": "Gdy to ważne",
"Writing": "Pisanie",
@@ -3068,6 +3124,7 @@ const LEMBAS_I18N = {
"About": "O aplikácii",
"Account": "Účet",
"Activity": "Aktivita",
"Add": "Pridať",
"Add folder…": "Pridať priečinok…",
"Address of your LLeMbas server": "Adresa vášho servera LLeMbas",
"Agent": "Agent",
@@ -3129,6 +3186,7 @@ const LEMBAS_I18N = {
"Counted by the model at the last reply; the categories are estimated.": "Spočítal model pri poslednej odpovedi; kategórie sú odhadnuté.",
"Create": "Vytvoriť",
"Cut short here; the whole of it is in the web interface.": "Tu skrátené; celé je to vo webovom rozhraní.",
"Data group": "Dátová oblasť",
"Days": "Dni",
"Delete": "Zmazať",
"Delete chat": "Zmazať konverzáciu",
@@ -3191,6 +3249,7 @@ const LEMBAS_I18N = {
"Manual — asks first": "Manual — najprv sa opýta",
"Max": "Max",
"Maximise": "Maximalizovať",
"Memory": "Pamäť",
"Memory and personal": "Pamäť a osobné",
"Menu": "Ponuka",
"Message": "Správa",
@@ -3225,10 +3284,12 @@ const LEMBAS_I18N = {
"Not measured.": "Nezmerané.",
"Not run yet": "Ešte nebežala",
"Not yet": "Ešte nie",
"Noted by a model": "Zaznamenal model",
"Nothing has been run in this shell yet.": "V tomto shelli sa zatiaľ nič nespustilo.",
"Nothing has been said in the room yet.": "V miestnosti zatiaľ nikto nič nepovedal.",
"Nothing is scheduled.": "Nič nie je naplánované.",
"Nothing matches.": "Nič nezodpovedá.",
"Nothing remembered yet.": "Zatiaľ si nič nepamätá.",
"Nothing running.": "Nič nebeží.",
"Nothing runs in the background. Subagents, a team's members and background commands show here while they work.": "Na pozadí nič nebeží. Podagenti, členovia tímu a príkazy na pozadí sa tu zobrazia, kým pracujú.",
"Nothing to report": "Nič na hlásenie",
@@ -3269,6 +3330,7 @@ const LEMBAS_I18N = {
"Regenerate reply": "Vygenerovať odpoveď znova",
"Remove": "Odstrániť",
"Remove %(dir)s": "Odstrániť %(dir)s",
"Remove this memory?": "Odstrániť tento záznam z pamäte?",
"Rename": "Premenovať",
"Rename chat": "Premenovať konverzáciu",
"Rename folder": "Premenovať zložku",
@@ -3312,6 +3374,7 @@ const LEMBAS_I18N = {
"Signing out gives the app's sign-in up on the server; the agent's link stays. Sign out to use another server.": "Odhlásením sa prihlásenie aplikácie na serveri zruší; prepojenie agenta zostane. Ak chcete použiť iný server, odhláste sa.",
"Since the last reply": "Od poslednej odpovede",
"Skipped": "Vynechané",
"Something to remember…": "Niečo na zapamätanie…",
"Source": "Zdroj",
"Start the agent": "Spustiť agenta",
"Stop": "Zastaviť",
@@ -3406,6 +3469,7 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Čaká sa na {{model}}: {{reason}}",
"What it does": "Čo robí",
"What should happen, and when": "Čo sa má stať a kedy",
"What your models remember about you, across chats. Change or remove anything here.": "Čo si o vás vaše modely pamätajú naprieč konverzáciami. Čokoľvek tu môžete zmeniť alebo odstrániť.",
"When": "Kedy",
"When it matters": "Keď na tom záleží",
"Writing": "Píše",
+10
View File
@@ -54,6 +54,7 @@
<button type="button" class="navrow" id="nav-scheduled" data-view="scheduled"><span class="navrow__icon"></span><span data-i18n>Scheduled</span></button>
<button type="button" class="navrow" id="nav-files" data-view="files"><span class="navrow__icon"></span><span data-i18n>Files</span></button>
<button type="button" class="navrow" id="nav-archived" data-view="archived"><span class="navrow__icon"></span><span data-i18n>Archived</span></button>
<button type="button" class="navrow" id="nav-memory" data-view="memory"><span class="navrow__icon"></span><span data-i18n>Memory</span></button>
</div>
<ul class="list" id="chat-list" aria-label="Chats" data-i18n-aria-label></ul>
<div class="side__foot">
@@ -133,6 +134,15 @@
<div class="page__body" id="archived-body"></div>
</section>
<section class="page" id="page-memory" hidden aria-labelledby="memory-title">
<header class="head">
<h1 class="head__title" id="memory-title" data-i18n>Memory</h1>
<div class="head__place"></div>
<div class="head__tools"><button type="button" class="tool" id="memory-refresh" aria-label="Read again" title="Read again" data-i18n-aria-label data-i18n-title></button></div>
</header>
<div class="page__body" id="memory-body"></div>
</section>
<section class="page settings" id="settings" hidden aria-labelledby="settings-title">
<header class="head">
<h1 class="head__title" id="settings-title" data-i18n>Settings</h1>
+20 -3
View File
@@ -10,7 +10,7 @@ import { renderBar, initBar } from "./titlebar.js"
import { renderSetup, initSetup } from "./setup.js"
import { renderSettings, initSettings, scrollToSection } from "./settings.js"
import { initViewer, onViewer, closeViewer } from "./viewer.js"
import { initExplorer, showLocal, showFilesTab } from "./explorer.js"
import { initExplorer, showLocal, showFilesTab, openServerFile } from "./explorer.js"
import { initList, refreshList, markCurrent } from "./list.js"
import { initChat, openChat, newChat, closeChat, openChatId, chatMenu, statusChanged, draftKind, onThisComputer, modelsChanged, intoComposer } from "./chat.js"
import { initTerminal, showTerminal, terminalOpen, terminalAvailable, toggleTerminal } from "./terminal.js"
@@ -21,12 +21,13 @@ import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
import { initAgent, link } from "./agent.js"
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
import { initArchived, showArchived } from "./archived.js"
import { showMemory } from "./memory.js"
import { closePopover } from "./popup.js"
import { noteSubagentsCap } from "./models.js"
let status = null
let view = "" // "setup" | "app"
let page = "chat" // "chat" | "files" | "scheduled" | "archived" | "settings"
let page = "chat" // "chat" | "files" | "scheduled" | "archived" | "memory" | "settings"
let me = null
let models = []
let devices = [] // this account's linked devices
@@ -156,6 +157,7 @@ function showPage(name) {
$("page-files").hidden = name !== "files"
$("page-scheduled").hidden = name !== "scheduled"
$("page-archived").hidden = name !== "archived"
$("page-memory").hidden = name !== "memory"
$("settings").hidden = name !== "settings"
for (const b of document.querySelectorAll(".navrow[data-view]")) b.setAttribute("aria-current", String(b.dataset.view === name))
if (name !== "chat") {
@@ -208,6 +210,11 @@ function showArchivedPage() {
showArchived()
}
function showMemoryPage(focus) {
showPage("memory")
showMemory(focus || "")
}
function showFiles(which) {
showPage("files")
if (which) showFilesTab(which)
@@ -223,6 +230,7 @@ function commands() {
...scheduledCommands(),
{ label: t("Files"), run: () => showFiles() },
{ label: t("Archived"), run: showArchivedPage },
{ label: t("Memory"), run: () => showMemoryPage() },
{ label: t("Settings"), run: () => showSettings(true) },
]
if (openChatId()) out.push({ label: panelOpen() ? t("Close the activity panel") : t("Open the activity panel"), run: togglePanel })
@@ -301,7 +309,12 @@ async function boot() {
// Back in the sidebar: the list again.
changed: () => refreshList(),
})
initPalette({ commands, open: (c) => showChat(c.id) })
initPalette({
commands,
open: (c) => showChat(c.id),
openFile: (path, name) => openServerFile(path, name),
openMemory: (id) => showMemoryPage(id),
})
initScheduled({ status: () => status, models: () => models, me: () => me, openChat: (id) => showChat(id), show: () => showPage("scheduled") })
onViewer((on) => {
$("shell").dataset.viewer = String(on)
@@ -340,6 +353,10 @@ async function boot() {
$("nav-archived").addEventListener("click", showArchivedPage)
$("archived-refresh").replaceChildren(icon(ICONS.refresh))
$("archived-refresh").addEventListener("click", () => showArchived())
$("nav-memory").querySelector(".navrow__icon").replaceChildren(icon(ICONS.bulb))
$("nav-memory").addEventListener("click", () => showMemoryPage())
$("memory-refresh").replaceChildren(icon(ICONS.refresh))
$("memory-refresh").addEventListener("click", () => showMemory())
accountButton()
$("settings-btn").addEventListener("click", () => showSettings(page !== "settings"))
$("settings-close").replaceChildren(icon(ICONS.close))
+159
View File
@@ -0,0 +1,159 @@
// Memory: what the account's models remember about the person, as the web's Settings → Memory
// keeps it. Each record can be changed or removed; a new one is added at the top, into the data
// group chosen (where there is more than one).
//
// GET /memories { memories: [memory], groups: [{ id, name }] }
// POST /memories { content, data_group_id? } -> { memory }
// PATCH /memories/{id} { content } -> { memory }
// DELETE /memories/{id} 204
//
// A record may be a model's words (`author` other than "user"): set as text, never markup.
import { $, h, clear, icon, ICONS, when } from "./dom.js"
import { get, post, patch, del, why } from "./bridge.js"
import { ask, menu } from "./popup.js"
let rows = []
let groups = []
let error = ""
let loading = false
let editing = "" // the record being changed
let mark = "" // the record to bring into view (from a search hit)
/** The page, read again from the server; `focus` a record to bring into view. */
export async function showMemory(focus = "") {
mark = focus
loading = true
error = ""
render()
try {
const r = await get("/memories")
rows = r.memories || []
groups = r.groups || []
} catch (err) {
rows = []
error = why(err)
}
loading = false
render()
}
async function add(form) {
const text = form.querySelector("textarea")
const content = text.value.trim()
if (!content) return text.focus()
const group = form.querySelector("select")?.value
form.querySelector("button[type=submit]").disabled = true
try {
const r = await post("/memories", group ? { content, data_group_id: group } : { content })
if (r?.memory) rows = [r.memory, ...rows]
error = ""
text.value = ""
} catch (err) {
error = why(err)
}
render()
$("memory-new")?.focus()
}
async function save(m, content) {
content = content.trim()
if (!content || content === m.content) {
editing = ""
return render()
}
try {
const r = await patch(`/memories/${encodeURIComponent(m.id)}`, { content })
rows = rows.map((x) => (x.id === m.id ? r?.memory || { ...x, content } : x))
editing = ""
error = ""
} catch (err) {
error = why(err)
}
render()
}
async function remove(m) {
const yes = await ask({ title: t("Remove this memory?"), text: m.content, ok: t("Remove"), cancel: t("Cancel"), danger: true })
if (!yes) return
try {
await del(`/memories/${encodeURIComponent(m.id)}`)
rows = rows.filter((x) => x.id !== m.id)
error = ""
} catch (err) {
error = why(err)
}
render()
}
function row(m) {
const sub = [groups.length > 1 ? m.data_group?.name : "", m.author && m.author !== "user" ? t("Noted by a model") : "", when(m.updated_at || m.created_at)].filter(Boolean).join(" · ")
if (editing === m.id) {
const box = h("textarea", { class: "textarea mrow__edit", rows: "3", "aria-label": t("Memory") })
box.value = m.content
const done = () => save(m, box.value)
box.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
e.stopPropagation()
editing = ""
render()
}
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) done()
})
queueMicrotask(() => box.focus())
return h(
"li",
{ class: "mrow is-editing", dataset: { id: m.id } },
box,
h(
"div",
{ class: "set__buttons" },
h("button", { type: "button", class: "btn btn--sm", text: t("Cancel"), onclick: () => ((editing = ""), render()) }),
h("button", { type: "button", class: "btn btn--sm btn--primary", text: t("Save"), onclick: done }),
),
)
}
return h(
"li",
{ class: `mrow${mark === m.id ? " is-marked" : ""}`, dataset: { id: m.id } },
h("div", { class: "mrow__words" }, h("p", { class: "mrow__text" }, h("bdi", { text: m.content })), h("p", { class: "mrow__sub", text: sub })),
h("button", { type: "button", class: "btn btn--sm", text: t("Edit"), onclick: () => ((editing = m.id), render()) }),
h(
"button",
{ type: "button", class: "tool", "aria-label": t("More"), title: t("More"), onclick: (e) => menu(e.currentTarget, [{ label: t("Remove"), danger: true, run: () => remove(m) }]) },
icon(ICONS.more),
),
)
}
function render() {
const box = clear($("memory-body"))
const parts = [h("p", { class: "muted", text: t("What your models remember about you, across chats. Change or remove anything here.") })]
const form = h(
"form",
{
class: "mnew",
onsubmit: (e) => {
e.preventDefault()
add(e.currentTarget)
},
},
h("textarea", { class: "textarea", id: "memory-new", rows: "2", placeholder: t("Something to remember…"), "aria-label": t("Something to remember…") }),
h(
"div",
{ class: "mnew__foot" },
groups.length > 1 ? h("select", { "aria-label": t("Data group") }, groups.map((g) => h("option", { value: g.id, text: g.name }))) : h("span"),
h("button", { type: "submit", class: "btn btn--sm btn--primary", text: t("Add") }),
),
)
form.querySelector("textarea").addEventListener("keydown", (e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) form.requestSubmit()
})
parts.push(form)
if (error) parts.push(h("p", { class: "status is-error", role: "alert", text: error }))
if (loading && !rows.length) parts.push(h("p", { class: "muted page__loading", text: t("Loading…") }))
else if (!rows.length && !error) parts.push(h("p", { class: "muted arch__empty", text: t("Nothing remembered yet.") }))
if (rows.length) parts.push(h("ul", { class: "arch mlist", "aria-label": t("Memory") }, rows.map(row)))
box.append(h("div", { class: "arch__page" }, parts))
if (mark) box.querySelector(`.mrow[data-id="${CSS.escape(mark)}"]`)?.scrollIntoView({ block: "center" })
}
+70 -8
View File
@@ -1,12 +1,16 @@
// The command palette (Ctrl+K): one line to type into, and what matches -- the app's commands,
// and the chats of both kinds by title, newest first. Up and Down move, Enter does it, Escape
// closes. The chats are asked of the server each time it opens, so a chat started elsewhere
// is there.
// the chats of both kinds by title, newest first, and below them what the server's search finds
// inside them, in the file library and in memory (`GET /search`, once two letters are typed and
// the typing pauses). Up and Down move, Enter does it, Escape closes. The chats are asked of the
// server each time it opens, so a chat started elsewhere is there.
//
// A hit's snippet is plain text with the ranges that matched (`marks`): drawn as text, the
// ranges as <mark> elements. Titles and snippets may be a model's words; nothing is markup.
import { h, clear, icon, ICONS, when } from "./dom.js"
import { get } from "./bridge.js"
let ctx = null // { commands(): [{ label, hint?, run }], open(id) }
let ctx = null // { commands(): [{ label, hint?, run }], open(chat), openFile(path, name), openMemory(id) }
let dlg = null
/** Lower case, without diacritics: "Príliš" is found by "prilis". */
@@ -17,6 +21,22 @@ function fold(text) {
.toLowerCase()
}
/** `text` with the `[start, end)` ranges of `marks` as <mark> elements: text nodes only. */
function marked(text, marks) {
const el = h("span", { class: "palette__snippet" })
let at = 0
const chars = [...String(text || "")]
for (const [a, b] of (marks || []).filter((m) => Array.isArray(m) && m[1] > m[0]).sort((x, y) => x[0] - y[0])) {
if (a < at) continue
el.append(chars.slice(at, a).join(""), h("mark", { text: chars.slice(a, b).join("") }))
at = b
}
el.append(chars.slice(at).join(""))
return el
}
const SEARCH_KIND = { chats: () => t("Chats"), files: () => t("Files"), memories: () => t("Memory") }
function matches(text, words) {
const f = fold(text)
return words.every((w) => f.includes(w))
@@ -31,7 +51,11 @@ export async function openPalette() {
let chats = []
let shown = []
let at = 0
let found = [] // the server's hits for `asked`
let asked = ""
let timer = 0
const close = () => {
clearTimeout(timer)
if (!dlg) return
dlg.close()
dlg.remove()
@@ -44,16 +68,32 @@ export async function openPalette() {
const draw = () => {
const words = fold(input.value).split(/\s+/).filter(Boolean)
const commands = ctx.commands().filter((c) => matches(c.label, words))
const found = chats.filter((c) => matches(c.title || t("New chat"), words)).slice(0, 30)
const titled = chats.filter((c) => matches(c.title || t("New chat"), words)).slice(0, 30)
shown = [
...commands.map((c) => ({ kind: "command", label: c.label, hint: c.hint || "", run: c.run })),
...found.map((c) => ({
...titled.map((c) => ({
kind: c.kind,
label: c.title || t("New chat"),
hint: `${c.kind === "agent" ? t("Agent") : t("Chats")} · ${when(c.updated_at)}`,
run: () => ctx.open(c),
})),
]
// What the server found inside: the chats already listed by their title once only.
const listed = new Set(titled.map((c) => c.id))
for (const group of input.value.trim() === asked ? found : []) {
for (const hit of group.hits || []) {
if (hit.kind === "chat" && listed.has(hit.id)) continue
const kind = { chat: "found-chat", file: "found-file", memory: "found-memory" }[hit.kind]
if (!kind) continue
shown.push({
kind,
label: hit.title || (hit.kind === "chat" ? t("New chat") : hit.kind === "file" ? String(hit.path || "").split("/").pop() : t("Memory")),
snippet: hit.snippet ? marked(hit.snippet, hit.marks) : null,
hint: SEARCH_KIND[group.kind]?.() || "",
run: () => (hit.kind === "chat" ? ctx.open({ id: hit.id, kind: hit.agent ? "agent" : "chat" }) : hit.kind === "file" ? ctx.openFile(hit.path, hit.title) : ctx.openMemory(hit.id)),
})
}
}
at = Math.min(at, Math.max(0, shown.length - 1))
clear(list)
if (!shown.length) list.append(h("li", { class: "palette__none", text: t("Nothing matches.") }))
@@ -74,8 +114,10 @@ export async function openPalette() {
}
},
},
h("span", { class: "palette__icon" }, icon(item.kind === "command" ? ICONS.chevron : item.kind === "agent" ? ICONS.folder : ICONS.file)),
h("span", { class: "palette__label", text: item.label }),
h("span", { class: "palette__icon" }, icon(item.kind === "command" ? ICONS.chevron : item.kind === "found-memory" ? ICONS.bulb : item.kind.startsWith("found") ? ICONS.search : item.kind === "agent" ? ICONS.folder : ICONS.file)),
item.snippet
? h("span", { class: "palette__label palette__label--two" }, h("span", { class: "palette__title", text: item.label }), item.snippet)
: h("span", { class: "palette__label", text: item.label }),
h("span", { class: "palette__hint", text: item.hint }),
),
)
@@ -87,9 +129,29 @@ export async function openPalette() {
input.setAttribute("aria-activedescendant", shown.length ? `palette-${at}` : "")
list.querySelector(`#palette-${at}`)?.scrollIntoView({ block: "nearest" })
}
// The server's search, once the typing pauses: an older server without it finds nothing more.
const search = () => {
clearTimeout(timer)
const q = input.value.trim()
if (q.length < 2) {
found = []
asked = ""
return
}
timer = setTimeout(async () => {
try {
const r = await get(`/search?q=${encodeURIComponent(q)}&in=all&limit=8`)
if (!dlg || input.value.trim() !== q) return
found = r.results || []
asked = q
draw()
} catch {}
}, 250)
}
input.addEventListener("input", () => {
at = 0
draw()
search()
})
input.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {