diff --git a/CHANGELOG.md b/CHANGELOG.md index e426326..4b353ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to LLeMbas Desktop. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added + +- **Archived chats.** Archive chat was one-way: an archived chat left the sidebar and could not be + found again in the app. Archived, under Files in the sidebar, lists them newest first; each opens + as any chat does, goes back into the sidebar with Restore chat, or is deleted. An archived chat + that is open says so in its header, with Restore chat beside it and in its menu. + ## [1.0.0-beta.6] — 2026-11-10 LLeMbas CLI 2.0.0-beta.6 inside, the look and words of LLeMbas 2.0.0-beta.8. diff --git a/scripts/gen-i18n.py b/scripts/gen-i18n.py index b65ae79..38dfb23 100755 --- a/scripts/gen-i18n.py +++ b/scripts/gen-i18n.py @@ -445,6 +445,10 @@ KEYS = ( "Stop %(name)s and the subagents under it?", "This device is not linked right now.", "Subagent %(name)s (%(id)s), %(position)s, asks:", + # archived.js + "Archived", + "Restore chat", + "No archived chats.", ) # The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the @@ -673,6 +677,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Dny", "Time": "Čas", "No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.", + "No archived chats.": "Žádné archivované konverzace.", }, "de": { "Sign in again: the server no longer accepts this app's sign-in.": "Melden Sie sich erneut an: Der Server nimmt die Anmeldung dieser App nicht mehr an.", @@ -896,6 +901,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Tage", "Time": "Uhrzeit", "No day ticked: every day.": "Kein Tag angehakt: jeden Tag.", + "No archived chats.": "Keine archivierten Chats.", }, "es": { "Sign in again: the server no longer accepts this app's sign-in.": "Vuelva a iniciar sesión: el servidor ya no acepta el inicio de sesión de esta aplicación.", @@ -1119,6 +1125,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Días", "Time": "Hora", "No day ticked: every day.": "Ningún día marcado: todos los días.", + "No archived chats.": "No hay chats archivados.", }, "fr": { "Sign in again: the server no longer accepts this app's sign-in.": "Reconnectez-vous : le serveur n’accepte plus la connexion de cette application.", @@ -1342,6 +1349,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Jours", "Time": "Heure", "No day ticked: every day.": "Aucun jour coché : tous les jours.", + "No archived chats.": "Aucune conversation archivée.", }, "hu": { "Sign in again: the server no longer accepts this app's sign-in.": "Jelentkezzen be újra: a szerver már nem fogadja el ennek az alkalmazásnak a bejelentkezését.", @@ -1565,6 +1573,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Napok", "Time": "Időpont", "No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.", + "No archived chats.": "Nincs archivált beszélgetés.", }, "it": { "Sign in again: the server no longer accepts this app's sign-in.": "Accedi di nuovo: il server non accetta più l’accesso di questa app.", @@ -1788,6 +1797,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Giorni", "Time": "Ora", "No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.", + "No archived chats.": "Nessuna chat archiviata.", }, "pl": { "Sign in again: the server no longer accepts this app's sign-in.": "Zaloguj się ponownie: serwer nie przyjmuje już logowania tej aplikacji.", @@ -2011,6 +2021,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Dni", "Time": "Godzina", "No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.", + "No archived chats.": "Brak zarchiwizowanych czatów.", }, "sk": { "Sign in again: the server no longer accepts this app's sign-in.": "Prihláste sa znova: server už neprijíma prihlásenie tejto aplikácie.", @@ -2234,6 +2245,7 @@ OWN: dict[str, dict[str, str]] = { "Days": "Dni", "Time": "Čas", "No day ticked: every day.": "Žiadny deň nezaškrtnutý: každý deň.", + "No archived chats.": "Žiadne archivované konverzácie.", }, } diff --git a/tests/ui/run.mjs b/tests/ui/run.mjs index 87233b2..92a98af 100644 --- a/tests/ui/run.mjs +++ b/tests/ui/run.mjs @@ -822,6 +822,71 @@ test("C4: the tree at 390 px stays inside the window", async () => { assert.deepEqual(p.errors, []) }) +// --- D: parity (beta.9) ------------------------------------------------------------------------ + +const shot = (p, name) => (process.env.SHOTS ? p.screenshot({ path: join(process.env.SHOTS, `${name}.png`) }) : null) +const noSideways = (p, width) => p.evaluate((w) => document.documentElement.scrollWidth <= w, width) + +function archivedRoutes(over = {}) { + return { + status: status(), + routes: baseRoutes({ + "GET /chats?archived=true&limit=50": { + chats: [ + chatRow({ id: "old1", title: "Lembas for the road, a recipe with a title long enough to be cut short", archived: true }), + agentChat({ id: "old2", title: "Old refactor", archived: true, running: false }), + ], + more: false, + }, + "PATCH /chats/old1": { chat: chatRow({ id: "old1", title: "Lembas for the road", archived: false }) }, + "GET /chats/old2": { chat: agentChat({ id: "old2", title: "Old refactor", archived: true, running: false }), messages: [userMessage("m9", "Tidy it")], more: false, live: null }, + "PATCH /chats/old2": { chat: agentChat({ id: "old2", title: "Old refactor", archived: false, running: false }) }, + ...over, + }), + streams: {}, + } +} + +test("D2: Archived lists the archived chats; Restore chat takes one back and the sidebar is read again", async () => { + const p = await page(archivedRoutes()) + await p.click("#nav-archived") + await p.waitForSelector("#page-archived:not([hidden]) .arow") + assert.equal(await p.locator("#page-archived .arow").count(), 2) + assert.match(await p.textContent('.arow[data-id="old2"]'), /project/, "an agent chat names its folder") + await shot(p, "d2-archived-1280") + const before = await p.evaluate(() => window.__mock.api("GET", "/chats?limit").length) + await p.click('.arow[data-id="old1"] .arow__unarchive') + await p.waitForFunction(() => window.__mock.api("PATCH", "/chats/old1").length === 1) + const [call] = await p.evaluate(() => window.__mock.api("PATCH", "/chats/old1")) + assert.deepEqual(call.args.body, { archived: false }) + await p.waitForFunction(() => !document.querySelector('.arow[data-id="old1"]')) + await p.waitForFunction((n) => window.__mock.api("GET", "/chats?limit").length > n, before) + assert.deepEqual(p.errors, []) +}) + +test("D2: an archived chat opens from the page and says so in its header; Restore chat there too", async () => { + const p = await page(archivedRoutes()) + await p.click("#nav-archived") + await p.waitForSelector('.arow[data-id="old2"] .arow__open') + await p.click('.arow[data-id="old2"] .arow__open') + await p.waitForSelector("#main:not([hidden]) #messages .msg") + await p.click("#chat-tools .head__unarchive") + await p.waitForFunction(() => window.__mock.api("PATCH", "/chats/old2").length === 1) + await p.waitForSelector("#chat-tools .head__unarchive", { state: "detached" }) + assert.deepEqual(p.errors, []) +}) + +test("D2: at 390 px the archived page stays inside the window, its buttons reachable", async () => { + const p = await page(archivedRoutes(), { width: 390, height: 844 }) + await p.evaluate(() => document.getElementById("nav-archived").click()) + await p.waitForSelector("#page-archived:not([hidden]) .arow") + assert.ok(await noSideways(p, 390), "nothing scrolls sideways") + const b = await p.locator('.arow[data-id="old1"] .arow__unarchive').boundingBox() + assert.ok(b && b.x + b.width <= 390, `Unarchive is on the screen: ${JSON.stringify(b)}`) + await shot(p, "d2-archived-390") + assert.deepEqual(p.errors, []) +}) + // --- Run ---------------------------------------------------------------------------------------- const only = process.argv[2] || "" diff --git a/ui/app.css b/ui/app.css index 3e4bdc5..221b981 100644 --- a/ui/app.css +++ b/ui/app.css @@ -1629,6 +1629,40 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); } .findings__item { min-width: 0; overflow-wrap: anywhere; } .ask__subagent { color: var(--ink); } +/* --- Archived chats: icon · title over its folder and date · Unarchive · more, on known tracks. */ + +.arch { list-style: none; margin: 0; padding: 0; display: grid; } +.arow { + display: grid; + grid-template-columns: 16px minmax(0, 1fr) auto var(--control-h-sm); + gap: var(--sp-3); + align-items: center; + min-height: var(--control-h); + padding: var(--sp-1) 0; + border-bottom: var(--border-w) solid var(--border); +} +.arow__icon { display: grid; place-items: center; color: var(--ink-faint); } +.arow__icon.is-agent { color: var(--leaf); } +.arow__icon .icon { width: 14px; height: 14px; } +.arow__open { + display: grid; + min-width: 0; + min-height: var(--control-h); + padding: var(--sp-1) 0; + text-align: left; + color: var(--ink); + background: transparent; + border: 0; + cursor: pointer; +} +.arow__open:hover .arow__title { text-decoration: underline; } +.arow__title, .arow__sub { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.arow__title { font: var(--text-sm) / var(--leading-tight) var(--font-body); } +.arow__sub { font: var(--text-xs) var(--font-ui); color: var(--ink-faint); } +.arch__more { margin-top: var(--sp-3); } +.arch__page { display: grid; gap: var(--sp-3); } +.head__unarchive { white-space: nowrap; } + /* --- Narrower windows ---------------------------------------------------------------------- */ /* 64rem (--bp-wide): a panel goes over the chat instead of beside it. */ diff --git a/ui/i18n.js b/ui/i18n.js index 9b45e71..47e2ede 100644 --- a/ui/i18n.js +++ b/ui/i18n.js @@ -55,6 +55,7 @@ const LEMBAS_I18N = { "Answer": "Odpovědět", "Approve this code in the page that opened in your browser:": "Potvrďte tento kód na stránce, která se otevřela ve vašem prohlížeči:", "Archive chat": "Archivovat konverzaci", + "Archived": "Archivované", "Attach files": "Přikládat soubory", "Auto — asks nothing": "Auto — na nic se neptá", "Automatic compaction is off.": "Automatické shrnutí je vypnuté.", @@ -168,6 +169,7 @@ const LEMBAS_I18N = { "New chat": "Nová konverzace", "New scheduled task": "Nová naplánovaná úloha", "Next run": "Další spuštění", + "No archived chats.": "Žádné archivované konverzace.", "No chat yet. Start one with New chat.": "Zatím žádná konverzace. Začněte ji tlačítkem Nová konverzace.", "No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.", "No files written in this chat yet.": "V tomto chatu se zatím nezapsal žádný soubor.", @@ -226,6 +228,7 @@ const LEMBAS_I18N = { "Rename chat": "Přejmenovat konverzaci", "Rename…": "Přejmenovat…", "Restore": "Obnovit", + "Restore chat": "Obnovit konverzaci", "Result": "Výsledek", "Resume": "Pokračovat", "Resume %(title)s": "Obnovit %(title)s", @@ -452,6 +455,7 @@ const LEMBAS_I18N = { "Answer": "Antworten", "Approve this code in the page that opened in your browser:": "Bestätigen Sie diesen Code auf der Seite, die sich in Ihrem Browser geöffnet hat:", "Archive chat": "Chat archivieren", + "Archived": "Archiviert", "Attach files": "Dateien anhängen", "Auto — asks nothing": "Auto — fragt nichts", "Automatic compaction is off.": "Die automatische Zusammenfassung ist aus.", @@ -565,6 +569,7 @@ const LEMBAS_I18N = { "New chat": "Neuer Chat", "New scheduled task": "Neue geplante Aufgabe", "Next run": "Nächster Lauf", + "No archived chats.": "Keine archivierten Chats.", "No chat yet. Start one with New chat.": "Noch kein Chat. Beginnen Sie einen mit Neuer Chat.", "No day ticked: every day.": "Kein Tag angehakt: jeden Tag.", "No files written in this chat yet.": "In diesem Chat wurde noch keine Datei geschrieben.", @@ -623,6 +628,7 @@ const LEMBAS_I18N = { "Rename chat": "Chat umbenennen", "Rename…": "Umbenennen…", "Restore": "Wiederherstellen", + "Restore chat": "Chat wiederherstellen", "Result": "Ergebnis", "Resume": "Fortsetzen", "Resume %(title)s": "%(title)s fortsetzen", @@ -849,6 +855,7 @@ const LEMBAS_I18N = { "Answer": "Responder", "Approve this code in the page that opened in your browser:": "Apruebe este código en la página que se abrió en su navegador:", "Archive chat": "Archivar chat", + "Archived": "Archivados", "Attach files": "Adjuntar archivos", "Auto — asks nothing": "Auto — no pregunta nada", "Automatic compaction is off.": "La compactación automática está desactivada.", @@ -962,6 +969,7 @@ const LEMBAS_I18N = { "New chat": "Nuevo chat", "New scheduled task": "Nueva tarea programada", "Next run": "Próxima ejecución", + "No archived chats.": "No hay chats archivados.", "No chat yet. Start one with New chat.": "Aún no hay chats. Empiece uno con Nuevo chat.", "No day ticked: every day.": "Ningún día marcado: todos los días.", "No files written in this chat yet.": "Todavía no se ha escrito ningún archivo en este chat.", @@ -1020,6 +1028,7 @@ const LEMBAS_I18N = { "Rename chat": "Renombrar chat", "Rename…": "Renombrar…", "Restore": "Restaurar", + "Restore chat": "Restaurar chat", "Result": "Resultado", "Resume": "Reanudar", "Resume %(title)s": "Reanudar %(title)s", @@ -1246,6 +1255,7 @@ const LEMBAS_I18N = { "Answer": "Répondre", "Approve this code in the page that opened in your browser:": "Approuvez ce code sur la page qui s'est ouverte dans votre navigateur :", "Archive chat": "Archiver la conversation", + "Archived": "Archivées", "Attach files": "Joindre des fichiers", "Auto — asks nothing": "Auto — ne demande rien", "Automatic compaction is off.": "Le compactage automatique est désactivé.", @@ -1359,6 +1369,7 @@ const LEMBAS_I18N = { "New chat": "Nouvelle conversation", "New scheduled task": "Nouvelle tâche planifiée", "Next run": "Prochaine exécution", + "No archived chats.": "Aucune conversation archivée.", "No chat yet. Start one with New chat.": "Aucune conversation pour l’instant. Commencez-en une avec Nouvelle conversation.", "No day ticked: every day.": "Aucun jour coché : tous les jours.", "No files written in this chat yet.": "Aucun fichier n’a encore été écrit dans cette conversation.", @@ -1417,6 +1428,7 @@ const LEMBAS_I18N = { "Rename chat": "Renommer la conversation", "Rename…": "Renommer…", "Restore": "Restaurer", + "Restore chat": "Restaurer la conversation", "Result": "Résultat", "Resume": "Reprendre", "Resume %(title)s": "Reprendre %(title)s", @@ -1643,6 +1655,7 @@ const LEMBAS_I18N = { "Answer": "Válasz", "Approve this code in the page that opened in your browser:": "Hagyja jóvá ezt a kódot a böngészőjében megnyílt oldalon:", "Archive chat": "Csevegés archiválása", + "Archived": "Archivált", "Attach files": "Fájlok csatolása", "Auto — asks nothing": "Auto — semmit nem kérdez", "Automatic compaction is off.": "Az automatikus tömörítés ki van kapcsolva.", @@ -1756,6 +1769,7 @@ const LEMBAS_I18N = { "New chat": "Új csevegés", "New scheduled task": "Új ütemezett feladat", "Next run": "Következő futás", + "No archived chats.": "Nincs archivált beszélgetés.", "No chat yet. Start one with New chat.": "Még nincs beszélgetés. Indítson egyet az Új beszélgetés gombbal.", "No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.", "No files written in this chat yet.": "Ebben a csevegésben még nem íródott fájl.", @@ -1814,6 +1828,7 @@ const LEMBAS_I18N = { "Rename chat": "Csevegés átnevezése", "Rename…": "Átnevezés…", "Restore": "Visszaállítás", + "Restore chat": "Csevegés visszaállítása", "Result": "Eredmény", "Resume": "Folytatás", "Resume %(title)s": "%(title)s folytatása", @@ -2040,6 +2055,7 @@ const LEMBAS_I18N = { "Answer": "Rispondi", "Approve this code in the page that opened in your browser:": "Approvi questo codice nella pagina che si è aperta nel suo browser:", "Archive chat": "Archivia chat", + "Archived": "Archiviate", "Attach files": "Allegare file", "Auto — asks nothing": "Auto — non chiede nulla", "Automatic compaction is off.": "La compattazione automatica è disattivata.", @@ -2153,6 +2169,7 @@ const LEMBAS_I18N = { "New chat": "Nuova chat", "New scheduled task": "Nuova attività pianificata", "Next run": "Prossima esecuzione", + "No archived chats.": "Nessuna chat archiviata.", "No chat yet. Start one with New chat.": "Ancora nessuna chat. Iniziane una con Nuova chat.", "No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.", "No files written in this chat yet.": "In questa chat non è ancora stato scritto alcun file.", @@ -2211,6 +2228,7 @@ const LEMBAS_I18N = { "Rename chat": "Rinomina chat", "Rename…": "Rinomina…", "Restore": "Ripristina", + "Restore chat": "Ripristina chat", "Result": "Risultato", "Resume": "Riprendi", "Resume %(title)s": "Riprendi %(title)s", @@ -2437,6 +2455,7 @@ const LEMBAS_I18N = { "Answer": "Odpowiedź", "Approve this code in the page that opened in your browser:": "Zatwierdź ten kod na stronie, która otworzyła się w przeglądarce:", "Archive chat": "Archiwizuj czat", + "Archived": "Zarchiwizowane", "Attach files": "Załączaj pliki", "Auto — asks nothing": "Auto — o nic nie pyta", "Automatic compaction is off.": "Automatyczne streszczanie jest wyłączone.", @@ -2550,6 +2569,7 @@ const LEMBAS_I18N = { "New chat": "Nowy czat", "New scheduled task": "Nowe zaplanowane zadanie", "Next run": "Następne uruchomienie", + "No archived chats.": "Brak zarchiwizowanych czatów.", "No chat yet. Start one with New chat.": "Jeszcze nie ma czatu. Zacznij go przyciskiem Nowy czat.", "No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.", "No files written in this chat yet.": "W tym czacie nie zapisano jeszcze żadnego pliku.", @@ -2608,6 +2628,7 @@ const LEMBAS_I18N = { "Rename chat": "Zmień nazwę czatu", "Rename…": "Zmień nazwę…", "Restore": "Przywróć", + "Restore chat": "Przywróć czat", "Result": "Wynik", "Resume": "Wznów", "Resume %(title)s": "Wznów %(title)s", @@ -2834,6 +2855,7 @@ const LEMBAS_I18N = { "Answer": "Odpovedať", "Approve this code in the page that opened in your browser:": "Potvrďte tento kód na stránke, ktorá sa otvorila vo vašom prehliadači:", "Archive chat": "Archivovať konverzáciu", + "Archived": "Archivované", "Attach files": "Prikladať súbory", "Auto — asks nothing": "Auto — na nič sa nepýta", "Automatic compaction is off.": "Automatické zhrnutie je vypnuté.", @@ -2947,6 +2969,7 @@ const LEMBAS_I18N = { "New chat": "Nová konverzácia", "New scheduled task": "Nová naplánovaná úloha", "Next run": "Ďalšie spustenie", + "No archived chats.": "Žiadne archivované konverzácie.", "No chat yet. Start one with New chat.": "Zatiaľ žiadna konverzácia. Začnite ju tlačidlom Nová konverzácia.", "No day ticked: every day.": "Žiadny deň nezaškrtnutý: každý deň.", "No files written in this chat yet.": "V tomto chate sa zatiaľ nezapísal žiadny súbor.", @@ -3005,6 +3028,7 @@ const LEMBAS_I18N = { "Rename chat": "Premenovať konverzáciu", "Rename…": "Premenovať…", "Restore": "Obnoviť", + "Restore chat": "Obnoviť konverzáciu", "Result": "Výsledok", "Resume": "Pokračovať", "Resume %(title)s": "Obnoviť %(title)s", diff --git a/ui/index.html b/ui/index.html index d21385f..2a17a70 100644 --- a/ui/index.html +++ b/ui/index.html @@ -53,6 +53,7 @@