import { html, nothing } from "lit"; import type { ArchivedSession, ArchivedSessionsResult } from "../controllers/sessions.ts"; import { formatRelativeTimestamp } from "../format.ts"; import { pathForTab } from "../navigation.ts"; import { formatSessionTokens } from "../presenter.ts"; import type { GatewaySessionRow, SessionsListResult } from "../types.ts"; export type AgentInfo = { id: string; name: string; emoji?: string; }; export type SessionsProps = { loading: boolean; result: SessionsListResult | null; error: string | null; activeMinutes: string; limit: string; includeGlobal: boolean; includeUnknown: boolean; basePath: string; agents?: AgentInfo[]; agentFilter?: string; onAgentFilterChange?: (agentId: string) => void; onFiltersChange: (next: { activeMinutes: string; limit: string; includeGlobal: boolean; includeUnknown: boolean; }) => void; onRefresh: () => void; onPatch: ( key: string, patch: { label?: string | null; thinkingLevel?: string | null; verboseLevel?: string | null; reasoningLevel?: string | null; }, ) => void; onDelete: (key: string) => void; onArchive: (key: string) => void; onViewHistory?: (key: string) => void; // Live sessions pagination livePageSize: number; livePage: number; onLivePageSizeChange: (size: number) => void; onLivePageChange: (page: number) => void; // Archived sessions props archivedLoading: boolean; archivedResult: ArchivedSessionsResult | null; archivedError: string | null; archivedSearch: string; archivedPageSize: number; archivedPage: number; onArchivedSearchChange: (search: string) => void; onArchivedRefresh: () => void; onArchivedPageSizeChange: (size: number) => void; onArchivedPageChange: (page: number) => void; onResumeSession: (sessionId: string) => void; onRenameSession: (sessionId: string, name: string) => void; onDeleteArchivedSession: (sessionId: string) => void; }; const THINK_LEVELS = ["", "off", "minimal", "low", "medium", "high", "xhigh"] as const; const BINARY_THINK_LEVELS = ["", "off", "on"] as const; const REASONING_LEVELS = ["", "off", "on", "stream"] as const; function normalizeProviderId(provider?: string | null): string { if (!provider) { return ""; } const normalized = provider.trim().toLowerCase(); if (normalized === "z.ai" || normalized === "z-ai") { return "zai"; } return normalized; } function isBinaryThinkingProvider(provider?: string | null): boolean { return normalizeProviderId(provider) === "zai"; } function resolveThinkLevelOptions(provider?: string | null): readonly string[] { return isBinaryThinkingProvider(provider) ? BINARY_THINK_LEVELS : THINK_LEVELS; } function withCurrentOption(options: readonly string[], current: string): string[] { if (!current || options.includes(current)) { return [...options]; } return [...options, current]; } function resolveThinkLevelDisplay(value: string, isBinary: boolean): string { if (!isBinary) { return value; } if (!value || value === "off") { return value; } return "on"; } function resolveThinkLevelPatchValue(value: string, isBinary: boolean): string | null { if (!value) { return null; } if (!isBinary) { return value; } if (value === "on") { return "low"; } return value; } // ── Session display name resolution ────────────────────────────── function resolveSessionFriendlyName(key: string, row: GatewaySessionRow): string { const label = row.label?.trim(); if (label && label !== key) { return label; } const dn = row.displayName?.trim(); if (dn && dn !== key) { return dn; } if (key === "main" || key.endsWith(":main")) { return "Main Session"; } if (key.includes(":cron:") && key.includes(":run:")) { return "Cron Run"; } if (key.includes(":cron:")) { return "Cron Job"; } if (key.includes(":subagent:")) { return "Subagent"; } if (key.includes(":openai:")) { return "OpenAI Session"; } const directMatch = key.match(/:([^:]+):direct:(.+)$/); if (directMatch) { return `${capitalize(directMatch[1])} · ${directMatch[2]}`; } const groupMatch = key.match(/:([^:]+):group:(.+)$/); if (groupMatch) { return `${capitalize(groupMatch[1])} Group`; } return key; } function capitalize(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); } function extractAgentId(key: string): string { const match = key.match(/^agent:([^:]+):/); return match ? match[1] : "unknown"; } function resolveAgentLabel(agentId: string, agents?: AgentInfo[]): string { if (!agents) { return agentId; } const agent = agents.find((a) => a.id === agentId); if (agent) { return `${agent.emoji ?? "🤖"} ${agent.name}`; } return agentId; } const PAGE_SIZES = [10, 20, 25] as const; function renderPaginationControls(opts: { total: number; page: number; pageSize: number; onPageChange: (page: number) => void; onPageSizeChange: (size: number) => void; }) { const totalPages = Math.max(1, Math.ceil(opts.total / opts.pageSize)); const page = Math.min(opts.page, totalPages); const start = (page - 1) * opts.pageSize + 1; const end = Math.min(page * opts.pageSize, opts.total); return html`
${opts.total > 0 ? `${start}–${end} of ${opts.total}` : "0 results"}
`; } export function renderSessions(props: SessionsProps) { const allRows = props.result?.sessions ?? []; const agents = props.agents ?? []; const agentFilter = props.agentFilter ?? ""; const filteredRows = agentFilter ? allRows.filter((row) => extractAgentId(row.key) === agentFilter) : allRows; // Client-side pagination for live sessions const liveTotal = filteredRows.length; const liveTotalPages = Math.max(1, Math.ceil(liveTotal / props.livePageSize)); const livePage = Math.min(props.livePage, liveTotalPages); const liveStart = (livePage - 1) * props.livePageSize; const rows = filteredRows.slice(liveStart, liveStart + props.livePageSize); return html`
Sessions
Inspect active sessions and adjust per-session defaults.
${ agents.length > 0 ? html` ` : nothing }
${ props.error ? html`
${props.error}
` : nothing }
${props.result ? `Store: ${props.result.path}` : ""} ${ agentFilter && liveTotal === 0 ? ` · No sessions for ${resolveAgentLabel(agentFilter, agents)}` : ` · ${liveTotal} session${liveTotal !== 1 ? "s" : ""}` }
Session
Agent
Kind
Updated
Tokens
Thinking
Reasoning
Actions
${ rows.length === 0 ? html`
${ agentFilter ? `No sessions found for ${resolveAgentLabel(agentFilter, agents)}.` : "No sessions found." }
` : rows.map((row) => renderRow( row, props.basePath, props.onPatch, props.onDelete, props.onArchive, props.loading, props.onViewHistory, agents, ), ) }
${liveTotal > 0 ? renderPaginationControls({ total: liveTotal, page: livePage, pageSize: props.livePageSize, onPageChange: props.onLivePageChange, onPageSizeChange: props.onLivePageSizeChange, }) : nothing}
${renderSessionHistory(props)} `; } function renderSessionHistory(props: SessionsProps) { const archivedSessions = props.archivedResult?.sessions ?? []; return html`
Session History
Browse and manage archived sessions.
${ props.archivedError ? html`
${props.archivedError}
` : nothing }
${props.archivedResult ? `${props.archivedResult.total} archived session${props.archivedResult.total !== 1 ? "s" : ""}` : ""}
Session
Agent
Archived
Messages
Actions
${ props.archivedLoading && archivedSessions.length === 0 ? html`
Loading archived sessions...
` : archivedSessions.length === 0 ? html`
No archived sessions found.
` : archivedSessions.map((session) => renderArchivedSessionRow(session, props)) }
${(props.archivedResult?.total ?? 0) > 0 ? renderPaginationControls({ total: props.archivedResult?.total ?? 0, page: props.archivedPage, pageSize: props.archivedPageSize, onPageChange: props.onArchivedPageChange, onPageSizeChange: props.onArchivedPageSizeChange, }) : nothing}
`; } function renderArchivedSessionRow(session: ArchivedSession, props: SessionsProps) { const displayName = session.displayName || (session.firstMessage ? session.firstMessage.slice(0, 50) + (session.firstMessage.length > 50 ? "..." : "") : session.sessionId); const archivedTime = formatRelativeTimestamp(session.archivedAt); function handleRename() { const newName = window.prompt("Enter new session name:", displayName); if (newName && newName !== displayName) { props.onRenameSession(session.sessionId, newName); } } return html`
${displayName} ${session.sessionKey}
${session.agentId}
${archivedTime}
${session.messageCount || 0}
`; } function renderRow( row: GatewaySessionRow, basePath: string, onPatch: SessionsProps["onPatch"], onDelete: SessionsProps["onDelete"], onArchive: SessionsProps["onArchive"], disabled: boolean, onViewHistory?: SessionsProps["onViewHistory"], agents?: AgentInfo[], ) { const updated = row.updatedAt ? formatRelativeTimestamp(row.updatedAt) : "n/a"; const rawThinking = row.thinkingLevel ?? ""; const isBinaryThinking = isBinaryThinkingProvider(row.modelProvider); const thinking = resolveThinkLevelDisplay(rawThinking, isBinaryThinking); const thinkLevels = withCurrentOption(resolveThinkLevelOptions(row.modelProvider), thinking); const reasoning = row.reasoningLevel ?? ""; const reasoningLevels = withCurrentOption(REASONING_LEVELS, reasoning); const friendlyName = resolveSessionFriendlyName(row.key, row); const isMainSession = row.key === "main" || row.key.endsWith(":main"); const agentId = extractAgentId(row.key); const agentLabel = resolveAgentLabel(agentId, agents); const canLink = row.kind !== "global"; const chatUrl = canLink ? `${pathForTab("chat", basePath)}?session=${encodeURIComponent(row.key)}` : null; return html`
${ canLink ? html`${friendlyName}` : html`${friendlyName}` } ${row.key}
${agentLabel}
${row.kind}
${updated}
${formatSessionTokens(row)}
${ onViewHistory ? html`` : nothing } ${ !isMainSession ? html`` : nothing }
`; }