feat(tui): add session navigation
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user