/** * Idle-session archive in the session view. A deployment may move chats that * have had no activity for a while into long-term storage: the conversation * stays readable, but the chat can no longer continue. This is separate from * the personal "Archive" action, which only hides a chat from your own list. * * - ReadOnlySessionNotice: the calm notice above a read-only chat's timeline. * - KeepActiveSetting: the per-chat exemption, a switch in the Agent tab. */ import { ArchiveIcon, PlusIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "../../lib/toast"; import { Button } from "../../../ui/button"; import { Notice } from "../../../ui/notice"; import { RelativeTime } from "../../../ui/relative-time"; import { SettingRow } from "../../../ui/setting-row"; import { Switch } from "../../../ui/switch"; import { useAppContext } from "../../app-context"; import { apiErrorFacts } from "../../../ui/api-error"; import { hasWorkspacePermission } from "../../lib/permissions"; import type { Session } from "../../types"; /** The chat's long-term storage state, or null while it is live. */ export function sessionReadOnlyArchive( session: Pick, ): NonNullable["archive"]> | null { return session.retention?.archive ?? null; } function idlePeriod(idleDays: number | undefined): string { if (!idleDays) return "a long time"; return `${idleDays} ${idleDays === 1 ? "day" : "days"}`; } export function ReadOnlySessionNotice(props: { session: Session; /** The deployment's idle period, when it still archives chats. */ idleDays?: number | undefined; onNewSession: () => void; }) { const archive = sessionReadOnlyArchive(props.session); if (!archive) return null; const moving = archive.state === "archiving"; return ( ); } /** * "Keep this chat active": exempts one chat from the idle-session archive, for * example a long-running agent that is only woken by schedules or messages. * Saves at once. Shown only on deployments that archive idle chats. */ export function KeepActiveSetting(props: { session: Session; idleDays: number; /** Re-reads the session after a save so every view shows the new value. */ onSaved?: (() => Promise) | undefined; }) { const context = useAppContext(); const { session } = props; const canEdit = hasWorkspacePermission( context.accessContext, session.workspaceId, "sessions:control", ); const keepLive = session.retention?.keepLive === true; const [pending, setPending] = useState(null); // The saved value until the session the panel shows has caught up with it. const [saved, setSaved] = useState(null); const [error, setError] = useState(null); const checked = pending ?? (saved !== null && saved !== keepLive ? saved : keepLive); useEffect(() => { if (saved !== null && saved === keepLive) setSaved(null); }, [keepLive, saved]); const save = async (next: boolean) => { setPending(next); setError(null); try { const updated = await context.client.updateSessionRetention(session.workspaceId, session.id, { keepLive: next, }); setSaved(updated.retention?.keepLive === true); context.setSession((current) => current?.id === updated.id && current.workspaceId === updated.workspaceId ? { ...current, retention: updated.retention } : current, ); void props.onSaved?.().catch(() => undefined); toast.success(next ? "This chat will stay active." : "This chat can become read-only."); } catch (caught) { const facts = apiErrorFacts(caught); setError( facts.code === "SESSION_ARCHIVED_READ_ONLY" || facts.status === 409 ? "This chat is already read-only, so it can't be kept active." : "Couldn't save. Try again.", ); } finally { setPending(null); } }; return ( void save(next)} /> } /> ); }