Archived chats: a page to find them again, Restore chat, delete
ci / ui (push) Waiting to run
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s

Archive was one-way in the app. Archived (sidebar, under Files) reads
GET /api/v2/chats?archived=true, newest first and paged; a row opens the
chat, Restore chat PATCHes archived:false and reads the sidebar again, and
the row's menu deletes. An open archived chat says so in its header, with
Restore chat there and in its menu. The words are the web's own ("Archived",
"Restore chat"). Tests: D2 at 1280 and 390 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 12:19:35 +00:00
1 parent cf13d49ed6
commit 94e903ef70
10 files changed
+323 -3

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
// Archived chats: the ones put away from the sidebar with Archive chat, newest first. Each can be
// opened (read, and written to, as any chat), taken back into the sidebar (Restore chat), or deleted.
//
// GET /chats?archived=true&limit=&before= { chats: [chat], more }
// PATCH /chats/{id} { archived: false } { chat }
// DELETE /chats/{id} 204
//
// A chat's title is the person's or a model's words, set as text.
import { $, h, clear, icon, ICONS, when } from "./dom.js"
import { get, patch, del, why } from "./bridge.js"
import { ask, menu } from "./popup.js"
import { folderName } from "./list.js"
let ctx = null // { open(id), changed(), me() }
let rows = []
let more = false
let error = ""
let loading = false
/** The page, read again from the server. */
export async function showArchived() {
loading = true
error = ""
render()
try {
const r = await get("/chats?archived=true&limit=50")
rows = r.chats || []
more = !!r.more
} catch (err) {
rows = []
more = false
error = why(err)
}
loading = false
render()
}
async function loadMore(button) {
const last = rows[rows.length - 1]
if (!last) return
button.disabled = true
try {
const r = await get(`/chats?archived=true&limit=50&before=${encodeURIComponent(last.id)}`)
rows = rows.concat(r.chats || [])
more = !!r.more
} catch (err) {
error = why(err)
}
render()
}
/** Back into the sidebar. */
export async function unarchive(chat) {
try {
const r = await patch(`/chats/${encodeURIComponent(chat.id)}`, { archived: false })
rows = rows.filter((x) => x.id !== chat.id)
error = ""
render()
ctx?.changed(r?.chat || chat)
return true
} catch (err) {
error = why(err)
render()
return false
}
}
async function remove(chat) {
const yes = await ask({
title: t("Delete this chat?"),
text: t("“%(title)s” is deleted on the server, with its messages and files. This cannot be undone.", { title: chat.title || t("New chat") }),
ok: t("Delete"),
cancel: t("Cancel"),
danger: true,
})
if (!yes) return
try {
await del(`/chats/${encodeURIComponent(chat.id)}`)
rows = rows.filter((x) => x.id !== chat.id)
error = ""
} catch (err) {
error = why(err)
}
render()
}
function row(c) {
const agent = c.kind === "agent"
const title = c.title || t("New chat")
const mayDelete = ctx.me()?.permissions?.["chat.delete"] !== false
const sub = [agent && c.device?.directory ? folderName(c.device.directory) : "", when(c.updated_at)].filter(Boolean).join(" · ")
return h(
"li",
{ class: "arow", dataset: { id: c.id } },
h("span", { class: `arow__icon${agent ? " is-agent" : ""}` }, icon(agent ? ICONS.terminal : ICONS.chat)),
h(
"button",
{ type: "button", class: "arow__open", title, onclick: () => ctx.open(c.id) },
h("span", { class: "arow__title" }, h("bdi", { text: title })),
h("span", { class: "arow__sub", text: sub }),
),
h("button", { type: "button", class: "btn btn--sm arow__unarchive", text: t("Restore chat"), onclick: () => unarchive(c) }),
mayDelete
? h(
"button",
{
type: "button",
class: "tool arow__more",
"aria-label": t("More"),
title: t("More"),
onclick: (e) => menu(e.currentTarget, [{ label: t("Delete chat"), danger: true, run: () => remove(c) }]),
},
icon(ICONS.more),
)
: h("span", { class: "arow__more" }),
)
}
function render() {
const box = clear($("archived-body"))
const parts = []
if (error) parts.push(h("p", { class: "status is-error", 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("No archived chats.") }))
if (rows.length) {
parts.push(h("ul", { class: "arch", "aria-label": t("Archived") }, rows.map(row)))
if (more) parts.push(h("button", { type: "button", class: "btn btn--sm arch__more", text: t("More"), onclick: (e) => loadMore(e.currentTarget) }))
}
box.append(h("div", { class: "arch__page" }, parts))
}
export function initArchived(context) {
ctx = context
}