import { useEffect, useState } from "react" import { Box, Text, useInput } from "ink" import { SelectableRows } from "./picker.js" export type TreeCheckpoint = { id: number type: "user" | "assistant" | "tool" preview: string timestamp?: number } /** Items are ordered from oldest to newest within this bounded page. */ export type TreePage = { items: TreeCheckpoint[] hasPrevious: boolean hasNext: boolean } export type TreePickerProps = { page?: TreePage loading?: boolean error?: string onPrevious: () => void onNext: () => void onFork: (checkpoint: TreeCheckpoint) => void | Promise onCancel: () => void } const previewLimit = 96 const readablePreview = (value: string): string => { const clean = value.replace(/[\p{Cc}\p{Cf}\p{Z}]+/gu, " ").trim() if (!clean) return "(empty)" const characters = [...clean] return characters.length <= previewLimit ? clean : `${characters.slice(0, previewLimit - 1).join("")}…` } const message = (error: unknown): string => error instanceof Error ? error.message : String(error) export function TreePicker({ page, loading = false, error = "", onPrevious, onNext, onFork, onCancel }: TreePickerProps) { const [selected, setSelected] = useState(0) const [confirming, setConfirming] = useState() const [forking, setForking] = useState(false) const [forkError, setForkError] = useState("") const items = page?.items ?? [] const index = Math.min(selected, Math.max(0, items.length - 1)) const current = items[index] useEffect(() => { setSelected(0) setConfirming(undefined) setForkError("") }, [page]) const fork = async (): Promise => { if (!current || forking) return setForkError("") setForking(true) try { await onFork(current) } catch (cause) { setForkError(message(cause)) setForking(false) } } useInput((input, key) => { if (key.escape || (key.ctrl && ["c", "d"].includes(input))) { if (confirming !== undefined) { setConfirming(undefined) setForkError("") } else onCancel() return } if (forking || loading) return if (confirming !== undefined) { if (key.return && current && confirming === current.id) void fork() return } if (key.leftArrow || key.pageUp) { if (page?.hasPrevious) onPrevious() return } if (key.rightArrow || key.pageDown) { if (page?.hasNext) onNext() return } if (key.upArrow || key.downArrow) { setSelected(Math.max(0, Math.min(items.length - 1, index + (key.upArrow ? -1 : 1)))) return } if (key.return && current) { setForkError("") setConfirming(current.id) } }) const confirmingCheckpoint = confirming === current?.id return albedo /tree · branch history choose the checkpoint the new session should end after {error && {error}} {loading || (!page && !error) ? loading history… : page && <> {items.length ? ({ id: String(checkpoint.id), label: <>{checkpoint.type.padEnd(9)} {readablePreview(checkpoint.preview)}, }))} /> : no branchable history in this session} {confirmingCheckpoint && current && branch after {current.type} · {readablePreview(current.preview)}? new session · fresh python namespace · workspace files stay unchanged {forkError && {forkError}} } } {forking ? "creating branch…" : confirmingCheckpoint ? `${forkError ? "enter retry" : "enter confirm"} · esc cancel` : "↑↓ select · ←→/pgup/pgdn page · enter branch · esc return to chat"} }