Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308package tui
import ( "albedo/cli/internal/daemon" "context" "errors" "fmt" "regexp" "strings"
tea "github.com/charmbracelet/bubbletea")
var whitespaceRegex = regexp.MustCompile(`[\p{Cc}\p{Cf}\p{Z}]+`)
func readablePreview(value string) string { clean := strings.TrimSpace(whitespaceRegex.ReplaceAllString(value, " ")) if clean == "" { return "(empty)" } runes := []rune(clean) if len(runes) <= 96 { return clean } return string(runes[:95]) + "…"}
type TreeCheckpoint struct { ID int `json:"id"` Type string `json:"type"` Preview string `json:"preview"` Timestamp *int64 `json:"timestamp,omitempty"`}
type TreeCancelMsg struct{}
type TreeForkSuccessMsg struct { Session daemon.Session}
type treeLoadedMsg struct { Items []TreeCheckpoint NextCursor *int HasMore bool Err error Gen int}
type treeForkedMsg struct { Session daemon.Session Err error Gen int}
type TreePickerModel struct { Conn *daemon.Connection SessionID string Cursors []int PageIndex int Checkpoints []TreeCheckpoint NextCursor *int HasMore bool Cursor int Confirming bool Forking bool Loading bool Error string ForkError string Generation int Width int Height int Styles Styles}
func NewTreePickerModel(conn *daemon.Connection, sessionID string) TreePickerModel { return TreePickerModel{ Conn: conn, SessionID: sessionID, Cursors: []int{0}, PageIndex: 0, Loading: true, Styles: DefaultStyles, }}
func (m *TreePickerModel) SetSize(width, height int) { m.Width = width m.Height = height}
func (m TreePickerModel) Init() tea.Cmd { return m.loadTreeCmd(0, m.Generation)}
func (m TreePickerModel) loadTreeCmd(after int, gen int) tea.Cmd { return func() tea.Msg { if m.Conn == nil { return treeLoadedMsg{Err: errors.New("daemon connection unavailable"), Gen: gen} }
// Check capability health, err := daemon.Request[struct { Capabilities []string `json:"capabilities"` }](context.Background(), m.Conn, "/health", nil) if err != nil { return treeLoadedMsg{Err: err, Gen: gen} } hasCap := false for _, c := range health.Capabilities { if c == "session_tree" { hasCap = true break } } if !hasCap { return treeLoadedMsg{ Err: errors.New("daemon upgrade needed for /tree; when ready, run albedo daemon --stop, then albedo (this clears python variables)"), Gen: gen, } }
type treeResp struct { Items []TreeCheckpoint `json:"items"` NextCursor *int `json:"nextCursor"` HasMore bool `json:"hasMore"` } path := fmt.Sprintf("/sessions/%s/tree?after=%d&limit=50", m.SessionID, after) resp, err := daemon.Request[treeResp](context.Background(), m.Conn, path, nil) if err != nil { return treeLoadedMsg{Err: err, Gen: gen} } return treeLoadedMsg{ Items: resp.Items, NextCursor: resp.NextCursor, HasMore: resp.HasMore, Gen: gen, } }}
func (m TreePickerModel) forkCmd(checkpointID int, gen int) tea.Cmd { return func() tea.Msg { if m.Conn == nil { return treeForkedMsg{Err: errors.New("daemon connection unavailable"), Gen: gen} } path := fmt.Sprintf("/sessions/%s/fork", m.SessionID) body := map[string]int{"checkpoint": checkpointID} branch, err := daemon.Request[daemon.Session](context.Background(), m.Conn, path, body) return treeForkedMsg{Session: branch, Err: err, Gen: gen} }}
func (m TreePickerModel) Update(msg tea.Msg) (TreePickerModel, tea.Cmd) { switch msg := msg.(type) { case treeLoadedMsg: if msg.Gen != m.Generation { return m, nil } m.Loading = false if msg.Err != nil { m.Error = msg.Err.Error() return m, nil } m.Checkpoints = msg.Items m.NextCursor = msg.NextCursor m.HasMore = msg.HasMore m.Cursor = 0 m.Confirming = false m.ForkError = "" m.Error = "" return m, nil
case treeForkedMsg: if msg.Gen != m.Generation { return m, nil } m.Forking = false if msg.Err != nil { m.ForkError = msg.Err.Error() return m, nil } branch := msg.Session return m, func() tea.Msg { return TreeForkSuccessMsg{Session: branch} }
case tea.KeyMsg: if msg.Type == tea.KeyEsc || msg.Type == tea.KeyCtrlC || msg.Type == tea.KeyCtrlD { if m.Confirming { m.Confirming = false m.Error = "" return m, nil } return m, func() tea.Msg { return TreeCancelMsg{} } }
if m.Forking || m.Loading { return m, nil }
if m.Confirming { if msg.Type == tea.KeyEnter && len(m.Checkpoints) > m.Cursor { cp := m.Checkpoints[m.Cursor] m.Forking = true m.ForkError = "" m.Error = "" m.Generation++ return m, m.forkCmd(cp.ID, m.Generation) } return m, nil }
switch msg.Type { case tea.KeyUp, tea.KeyCtrlP: if m.Cursor > 0 { m.Cursor-- } case tea.KeyDown, tea.KeyCtrlN: if m.Cursor < len(m.Checkpoints)-1 { m.Cursor++ } case tea.KeyLeft, tea.KeyPgUp: if m.PageIndex > 0 { m.PageIndex-- m.Loading = true m.Error = "" m.Generation++ return m, m.loadTreeCmd(m.Cursors[m.PageIndex], m.Generation) } case tea.KeyRight, tea.KeyPgDown: if m.HasMore && m.NextCursor != nil { next := *m.NextCursor m.Cursors = append(m.Cursors[:m.PageIndex+1], next) m.PageIndex++ m.Loading = true m.Error = "" m.Generation++ return m, m.loadTreeCmd(next, m.Generation) } case tea.KeyEnter: if len(m.Checkpoints) > 0 && m.Cursor < len(m.Checkpoints) { m.ForkError = "" m.Confirming = true } } } return m, nil}
func (m TreePickerModel) View() string { var b strings.Builder
b.WriteString(titleRule(m.Width, brand("albedo")+" "+m.Styles.Muted.Render("/tree"), m.Styles.Faint.Render("branch history"))) b.WriteString("\n") b.WriteString(m.Styles.Faint.Render(inkWrap("choose the checkpoint the new session should end after", m.Width))) b.WriteString("\n")
if m.Error != "" { b.WriteString(DefaultStyles.Error.Render("error:") + " " + m.Error) b.WriteByte('\n') } if m.Loading || len(m.Checkpoints) == 0 { if m.Error == "" && !m.Loading { b.WriteString(m.Styles.Faint.Render("no branchable history in this session")) b.WriteByte('\n') } if m.Error == "" && m.Loading { b.WriteString(m.Styles.Faint.Render("loading history…")) b.WriteByte('\n') } b.WriteString(keyHints(hint{"enter", "confirm"}, hint{"esc", "cancel"})) return b.String() }
lines := make([]string, len(m.Checkpoints)) for i, cp := range m.Checkpoints { lines[i] = DefaultStyles.Faint.Render(fmt.Sprintf("%-9s", cp.Type)) + " " + readablePreview(cp.Preview) } b.WriteString(selectableRows(lines, m.Cursor, m.Height, m.Height, m.Width, m.Styles)) b.WriteByte('\n')
if m.Confirming && m.Cursor < len(m.Checkpoints) { current := m.Checkpoints[m.Cursor] b.WriteString("\n") b.WriteString(DefaultStyles.Warning.Render(fmt.Sprintf("branch after %s · %s?", current.Type, readablePreview(current.Preview)))) b.WriteString("\n") b.WriteString(m.Styles.Faint.Render("new session · fresh python namespace · workspace files stay unchanged") + "\n") if m.ForkError != "" { b.WriteString(DefaultStyles.Error.Render(m.ForkError)) b.WriteByte('\n') } }
if m.Forking { b.WriteString(m.Styles.Faint.Render("creating branch…")) } else if m.Confirming { confirm := hint{"enter", "confirm"} if m.ForkError != "" { confirm = hint{"enter", "retry"} } b.WriteString(keyHints(confirm, hint{"esc", "cancel"})) } else { b.WriteString(inkWrap(keyHints(hint{"↑↓", "select"}, hint{"←→/pgup/pgdn", "page"}, hint{"enter", "branch"}, hint{"esc", "return to chat"}), m.Width)) }
return b.String()}