Files
nanobot/tui/src/session-menu.ts
T

76 lines
2.4 KiB
TypeScript

import { type BoxRenderable, type CliRenderer } from "@opentui/core"
import { PickerMenu, type PickerMenuTheme } from "./picker-menu"
import type { SessionSummary } from "./protocol"
type SessionMenuRow = SessionSummary & { active: boolean }
function sessionLabel(session: SessionSummary): string {
return session.title.trim() || session.preview.trim() || "Untitled chat"
}
function updatedLabel(value: string | null): string {
if (!value) return ""
const date = new Date(value)
if (Number.isNaN(date.valueOf())) return ""
const age = Math.max(0, Date.now() - date.valueOf())
if (age < 60_000) return "now"
if (age < 3_600_000) return `${Math.floor(age / 60_000)}m`
if (age < 86_400_000) return `${Math.floor(age / 3_600_000)}h`
return `${Math.floor(age / 86_400_000)}d`
}
/** Searchable session navigation over the gateway-owned session list. */
export class SessionMenu {
readonly root: BoxRenderable
private readonly picker: PickerMenu<SessionMenuRow>
constructor(renderer: CliRenderer, theme: PickerMenuTheme) {
this.picker = new PickerMenu(renderer, theme, {
id: "nanobot-tui-session-menu",
searchText: (session) => `${sessionLabel(session)} ${session.preview} ${session.chatId}`,
render: (session) => {
const age = updatedLabel(session.updatedAt)
const preview = session.preview.trim()
const detail = [age, preview && preview !== sessionLabel(session) ? preview : ""]
.filter(Boolean)
.join(" · ")
return `${session.active ? "● " : ""}${sessionLabel(session)}${detail ? ` ${detail}` : ""}`
},
emptyText: "No matching sessions",
})
this.root = this.picker.root
}
get visible(): boolean {
return this.picker.visible
}
open(sessions: SessionSummary[], currentChatId: string, limit: number): void {
const rows = sessions
.map((session) => ({ ...session, active: session.chatId === currentChatId }))
.sort((left, right) => Number(right.active) - Number(left.active))
this.picker.show(rows, "", limit)
}
update(query: string, limit: number): void {
this.picker.update(query, limit)
}
move(direction: -1 | 1): boolean {
return this.picker.move(direction)
}
choose(): SessionSummary | null {
return this.picker.current()
}
hide(): void {
this.picker.hide()
}
setTheme(theme: PickerMenuTheme): void {
this.picker.setTheme(theme)
}
}