import { deriveSessionDisplayTitle, type OpenGeniClient, type Session } from "@opengeni/sdk"; import { ArchiveIcon, PencilIcon, PlusIcon } from "lucide-react"; import { useEffect, useRef, useState, type FormEvent } from "react"; import { useClientConfigFlags } from "../hooks/use-client-config-flags"; import { useWorkspaceSessions } from "../hooks/use-workspace-sessions"; import { cn } from "../lib/cn"; import { formatRelativeTime } from "../lib/format"; import { useErrorMessage } from "../lib/error-message"; import { useOpenGeni, type ClientOverride } from "../session-context"; import { useOpenGeniText, type OpenGeniText } from "../messages/runtime"; export type SessionListLabels = { heading: string; newChat: string; empty: string; loading: string; loadMore: string; rename: string; archive: string; save: string; cancel: string; }; const DEFAULT_LABELS = (t: OpenGeniText): SessionListLabels => ({ heading: t("sessionList.chats"), newChat: t("sessionList.newChat"), empty: t("sessionList.noChatsYet"), loading: t("sessionList.loadingChats"), loadMore: t("sessionList.showMore"), rename: t("sessionList.rename"), archive: t("sessionList.archive"), save: t("sessionList.save"), cancel: t("sessionList.cancel"), }); export type SessionListProps = ClientOverride & { selectedSessionId?: string | null | undefined; onSelect: (sessionId: string) => void; /** Shown as a "New chat" button when provided. */ onNewChat?: (() => void) | undefined; /** Allow inline rename (`updateSession`). Defaults to true. */ rename?: boolean | undefined; /** * Allow archiving (`updateSessionArchive`). Defaults to true; hidden when the * session proxy reports archive disabled (`archive: false`). */ archive?: boolean | undefined; /** Called after the selected chat was archived, so the host can move on. */ onArchived?: ((sessionId: string) => void) | undefined; pageSize?: number | undefined; /** Change the reference to force a refetch (for example after creating a chat). */ refreshKey?: unknown; labels?: Partial | undefined; className?: string | undefined; }; type ArchiveClient = Partial>; /** * The workspace's root chats for the current user, newest activity first. * Through `createSessionProxyHandler` the list is limited to the chats the * resolved user created (or everything they may read, with `sessionList: * "visible"`); Opengeni enforces visibility either way. */ export function SessionList({ client, workspaceId, selectedSessionId, onSelect, onNewChat, rename = true, archive = true, onArchived, pageSize = 30, refreshKey, labels: labelOverrides, className, }: SessionListProps) { const t = useOpenGeniText(); const labels = { ...DEFAULT_LABELS(t), ...labelOverrides }; const scope = { client, workspaceId }; const context = useOpenGeni(scope); const [limit, setLimit] = useState(pageSize); const list = useWorkspaceSessions({ ...scope, parentSessionId: null, archiveStatus: "active", sortBy: "updatedAt", limit, }); const refresh = list.refresh; const firstRefreshKey = useRef(refreshKey); useEffect(() => { if (Object.is(refreshKey, firstRefreshKey.current)) return; firstRefreshKey.current = refreshKey; void refresh(); }, [refresh, refreshKey]); const [editing, setEditing] = useState<{ id: string; title: string } | null>(null); const [busy, setBusy] = useState(null); const [error, setError] = useState<{ cause: unknown } | null>(null); const formatError = useErrorMessage(); const archiveClient = context.client as unknown as ArchiveClient; // A session proxy with `archive: false` reports it; hide what would fail. const config = useClientConfigFlags(context.client as never); const canArchive = archive && config.archive && typeof archiveClient.updateSessionArchive === "function"; const run = async (id: string, action: () => Promise) => { setBusy(id); setError(null); try { await action(); await list.refresh(); } catch (cause) { setError({ cause }); } finally { setBusy(null); } }; const saveTitle = (event: FormEvent) => { event.preventDefault(); const current = editing; if (!current) return; const title = current.title.trim(); setEditing(null); if (!title) return; void run(current.id, () => context.client.updateSession(context.workspaceId, current.id, { title }), ); }; const archiveSession = (session: Session) => void run(session.id, async () => { await archiveClient.updateSessionArchive!(context.workspaceId, session.id, { archived: true, }); onArchived?.(session.id); }); const sessions = list.sessions; return ( ); }