import type { ProgressCard } from "@openclaw/gateway-protocol"; import { html, nothing } from "lit"; import type { ControlUiSessionPullRequest, ControlUiSessionPullRequestSnapshot, } from "../../../src/gateway/control-ui-contract.js"; import { t } from "../i18n/index.ts"; import { formatRelativeTimestamp } from "../lib/format.ts"; import type { SidebarRecentSession } from "./app-sidebar-session-types.ts"; import { sessionOwnerInitials } from "./session-owner-chip.ts"; import { renderSessionProgressCard } from "./session-progress-card.ts"; const MAX_VISIBLE_PULL_REQUESTS = 3; type SessionHovercardAvatarAuth = { authTokens: readonly string[]; authReady: boolean; }; let channelAvatarElementLoad: Promise | undefined; function ensureChannelAvatarElement(): void { channelAvatarElementLoad ??= import("./channel-avatar.ts"); } function pullRequestStateLabel(state: ControlUiSessionPullRequest["state"]): string { return t(`sessionHovercard.states.${state}`); } function checksPresentation(checks: NonNullable): { glyph: string; label: string; } { switch (checks.state) { case "passing": return { glyph: "✓", label: t("sessionHovercard.checks.passing") }; case "failing": return { glyph: "✗", label: t("sessionHovercard.checks.failing") }; case "pending": return { glyph: "○", label: t("sessionHovercard.checks.pending") }; default: return checks.state satisfies never; } } function changedFilesLabel(changedFiles: number): string { return t(changedFiles === 1 ? "sessionHovercard.changedFile" : "sessionHovercard.changedFiles", { count: String(changedFiles), }); } function renderDiffStats(item: { additions?: number; deletions?: number; changedFiles?: number }) { if ( item.additions === undefined && item.deletions === undefined && item.changedFiles === undefined ) { return nothing; } return html` ${item.changedFiles === undefined ? nothing : html`${changedFilesLabel(item.changedFiles)}`} ${item.additions === undefined ? nothing : html`+${item.additions.toLocaleString()}`} ${item.deletions === undefined ? nothing : html`−${item.deletions.toLocaleString()}`} `; } function renderHeader( row: SidebarRecentSession | undefined, channelAvatarAuth: SessionHovercardAvatarAuth | undefined, ) { if (!row) { return nothing; } const owner = row.owner?.actor ?? row.createdActor; const ownerLabel = owner?.label?.trim() || owner?.id?.trim(); const initials = owner ? sessionOwnerInitials(owner) : ""; const avatarFallback = initials ? html`` : nothing; const created = formatRelativeTimestamp(row.startedAt, { fallback: "" }); const updated = formatRelativeTimestamp(row.updatedAt, { fallback: "" }); const metadata = [ ownerLabel, created ? t("sessionHovercard.created", { time: created }) : undefined, updated ? t("sessionHovercard.updated", { time: updated }) : undefined, ].filter((value): value is string => Boolean(value)); if (row.channelAvatarUrl) { ensureChannelAvatarElement(); } return html`
${row.channelAvatarUrl ? html`` : initials ? html`` : nothing} ${row.label} ${metadata.length > 0 ? html`${metadata.join(" · ")}` : nothing}
`; } function renderPullRequestChip(pullRequest: ControlUiSessionPullRequest) { const state = pullRequestStateLabel(pullRequest.state); const checks = pullRequest.checks ? checksPresentation(pullRequest.checks) : null; return html` #${pullRequest.number} ${state} ${checks ? html`${checks.glyph}` : nothing} ${renderDiffStats(pullRequest)} `; } function renderPullRequestDetails(snapshot: ControlUiSessionPullRequestSnapshot | undefined) { if (!snapshot) { return nothing; } if (snapshot.pullRequests.length > 0) { const visible = snapshot.pullRequests.slice(0, MAX_VISIBLE_PULL_REQUESTS); const hiddenCount = snapshot.pullRequests.length - visible.length; return html`
${visible.map(renderPullRequestChip)} ${hiddenCount > 0 ? html`${t("sessionHovercard.more", { count: String(hiddenCount) })}` : nothing}
`; } const branch = snapshot.branch; if (!branch) { return nothing; } const noPullRequest = t("sessionHovercard.noPrYet"); return html`
${branch.owner}/${branch.repo} · ${branch.branch} ${renderDiffStats(branch)}
${branch.createUrl ? html`${noPullRequest}` : noPullRequest}
`; } export function renderSessionHovercard(input: { row?: SidebarRecentSession; channelAvatarAuth?: SessionHovercardAvatarAuth; pullRequests?: ControlUiSessionPullRequestSnapshot; progressCard?: ProgressCard | null; }) { const hasPullRequestDetails = Boolean( input.pullRequests && (input.pullRequests.pullRequests.length > 0 || input.pullRequests.branch), ); const lastMessagePreview = input.progressCard ? undefined : input.row?.lastMessagePreview?.trim() || undefined; if (!input.row && !hasPullRequestDetails && !input.progressCard) { return nothing; } return html`
${renderHeader(input.row, input.channelAvatarAuth)} ${renderPullRequestDetails(input.pullRequests)} ${input.progressCard ? html` ${renderSessionProgressCard(input.progressCard, "hovercard")}` : lastMessagePreview ? html`
${lastMessagePreview}
` : nothing}
`; }