okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawntui: sessions and folders fuzzy filter, shared confirm and footers10e0f66

main
3.7 KiB90 linesraw
1// The session list in every state a person sees, for the shot gallery. A
2// plain test run draws each state and writes nothing.
3package tui
4
5import (
6	"time"
7
8	"albedo/cli/internal/daemon"
9
10	tea "charm.land/bubbletea/v2"
11)
12
13func init() {
14	enter := tea.KeyPressMsg{Code: tea.KeyEnter}
15	ctrl := func(r rune) tea.Msg { return tea.KeyPressMsg{Code: r, Mod: tea.ModCtrl} }
16	registerShots("sessions",
17		shotState{"list", sessionsShot(nil)},
18		shotState{"filtering", sessionsShot(nil, typedKeys("warm")...)},
19		shotState{"no-match", sessionsShot(nil, typedKeys("zzz")...)},
20		shotState{"action-row", sessionsShot(func(m *SessionViewer) { m.focus("new") })},
21		shotState{"preview-loading", sessionsShot(func(m *SessionViewer) { m.focus("week") })},
22		shotState{"renaming", sessionsShot(nil, ctrl('r'))},
23		shotState{"archive", sessionsShot(func(m *SessionViewer) { m.OpenArchive() })},
24		shotState{"archive-confirm", sessionsShot(func(m *SessionViewer) { m.OpenArchive() }, ctrl('d'))},
25		shotState{"archive-empty", sessionsShot(func(m *SessionViewer) {
26			m.SetSessions([]daemon.Session{{ID: "pin", Title: "Only live session"}}, nil)
27			m.OpenArchive()
28		})},
29		shotState{"notice", sessionsShot(func(m *SessionViewer) {
30			m.notice = "could not rename: the daemon refused the name"
31		})},
32		shotState{"loading", func(width, height int) string {
33			m := NewSessionViewer("/Users/dawn/proj/albedo")
34			m.SetSize(width, height)
35			return m.View()
36		}},
37		shotState{"empty", func(width, height int) string {
38			m := sessionsModel(width, height, nil)
39			m.SetSessions(nil, nil)
40			return m.View()
41		}},
42		shotState{"confirm-enter", sessionsShot(func(m *SessionViewer) { m.OpenArchive() }, ctrl('d'), enter)},
43	)
44}
45
46// typedKeys is the keystrokes that type text into a picker.
47func typedKeys(text string) []tea.Msg {
48	var keys []tea.Msg
49	for _, r := range text {
50		keys = append(keys, tea.KeyPressMsg{Code: r, Text: string(r)})
51	}
52	return keys
53}
54
55// sessionsModel is the list at a terminal size, with the fixtures every
56// state shares, and setup applied before any keys.
57func sessionsModel(width, height int, setup func(*SessionViewer)) SessionViewer {
58	now := time.Date(2026, 9, 23, 12, 0, 0, 0, time.Local)
59	recent, older := now.Add(-5*time.Minute).Unix(), now.Add(-3*24*time.Hour).Unix()
60	m := viewerAt(now)
61	m.SetSize(width, height)
62	m.Fetch = func(string) tea.Cmd { return nil }
63	m.SetSessions([]daemon.Session{
64		{ID: "pin", Title: "Refactor the session viewer", Model: "claude-sonnet-4", Workspace: "/Users/dawn/proj/albedo", LastAssistantAt: &recent, Pinned: true, Cursor: &daemon.Cursor{}, TranscriptCount: 42},
65		{ID: "live", Title: "Fix flaky warm test", Model: "gpt-5", Workspace: "/Users/dawn/proj/albedo", LastAssistantAt: &recent, Cursor: &daemon.Cursor{}, TranscriptCount: 9},
66		{ID: "week", Title: "Read the folder picker spec", Model: "claude-opus-4", Workspace: "/Users/dawn/proj/other", LastAssistantAt: &older, TranscriptCount: 3},
67		{ID: "old", Title: "Design notes for the archive", Model: "gpt-5", Workspace: "/Users/dawn/proj/albedo", Archived: true},
68	}, nil)
69	m.focus("live")
70	m.previews["live"] = &cachedPreview{stamp: recent, SessionPreview: daemon.SessionPreview{Items: []daemon.PreviewItem{
71		{Type: "user", Preview: "why does the warm test flake under load"},
72		{Type: "tool", Preview: "read"}, {Type: "tool", Preview: "read"}, {Type: "tool", Preview: "bash"},
73		{Type: "assistant", Preview: "the cache ttl was racing the stamp"},
74	}}}
75	if setup != nil {
76		setup(&m)
77	}
78	return m
79}
80
81// sessionsShot draws the list after setup and keys.
82func sessionsShot(setup func(*SessionViewer), keys ...tea.Msg) func(width, height int) string {
83	return func(width, height int) string {
84		m := sessionsModel(width, height, setup)
85		for _, key := range keys {
86			m, _ = m.Update(key)
87		}
88		return m.View()
89	}
90}