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`
`;
}
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`
${agentLabel}
${row.kind}
${updated}
${formatSessionTokens(row)}
${
onViewHistory
? html``
: nothing
}
${
!isMainSession
? html``
: nothing
}
`;
}