sessions.go

  1package repl
  2
  3import (
  4	"fmt"
  5
  6	"github.com/charmbracelet/bubbles/key"
  7	"github.com/charmbracelet/bubbles/list"
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/kujtimiihoxha/termai/internal/app"
 10	"github.com/kujtimiihoxha/termai/internal/pubsub"
 11	"github.com/kujtimiihoxha/termai/internal/session"
 12	"github.com/kujtimiihoxha/termai/internal/tui/layout"
 13	"github.com/kujtimiihoxha/termai/internal/tui/styles"
 14	"github.com/kujtimiihoxha/termai/internal/tui/util"
 15)
 16
 17type SessionsCmp interface {
 18	tea.Model
 19	layout.Sizeable
 20	layout.Focusable
 21	layout.Bordered
 22	layout.Bindings
 23}
 24type sessionsCmp struct {
 25	app     *app.App
 26	list    list.Model
 27	focused bool
 28}
 29
 30type listItem struct {
 31	id, title, desc string
 32}
 33
 34func (i listItem) Title() string       { return i.title }
 35func (i listItem) Description() string { return i.desc }
 36func (i listItem) FilterValue() string { return i.title }
 37
 38type InsertSessionsMsg struct {
 39	sessions []session.Session
 40}
 41
 42type SelectedSessionMsg struct {
 43	SessionID string
 44}
 45
 46type sessionsKeyMap struct {
 47	Select key.Binding
 48}
 49
 50var sessionKeyMapValue = sessionsKeyMap{
 51	Select: key.NewBinding(
 52		key.WithKeys("enter", " "),
 53		key.WithHelp("enter/space", "select session"),
 54	),
 55}
 56
 57func (i *sessionsCmp) Init() tea.Cmd {
 58	existing, err := i.app.Sessions.List()
 59	if err != nil {
 60		return util.ReportError(err)
 61	}
 62	if len(existing) == 0 || existing[0].MessageCount > 0 {
 63		newSession, err := i.app.Sessions.Create(
 64			"New Session",
 65		)
 66		if err != nil {
 67			return util.ReportError(err)
 68		}
 69		existing = append([]session.Session{newSession}, existing...)
 70	}
 71	return tea.Batch(
 72		util.CmdHandler(InsertSessionsMsg{existing}),
 73		util.CmdHandler(SelectedSessionMsg{existing[0].ID}),
 74	)
 75}
 76
 77func (i *sessionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 78	switch msg := msg.(type) {
 79	case InsertSessionsMsg:
 80		items := make([]list.Item, len(msg.sessions))
 81		for i, s := range msg.sessions {
 82			items[i] = listItem{
 83				id:    s.ID,
 84				title: s.Title,
 85				desc:  fmt.Sprintf("Tokens: %d, Cost: %.2f", s.PromptTokens+s.CompletionTokens, s.Cost),
 86			}
 87		}
 88		return i, i.list.SetItems(items)
 89	case pubsub.Event[session.Session]:
 90		if msg.Type == pubsub.UpdatedEvent {
 91			// update the session in the list
 92			items := i.list.Items()
 93			for idx, item := range items {
 94				s := item.(listItem)
 95				if s.id == msg.Payload.ID {
 96					s.title = msg.Payload.Title
 97					s.desc = fmt.Sprintf("Tokens: %d, Cost: %.2f", msg.Payload.PromptTokens+msg.Payload.CompletionTokens, msg.Payload.Cost)
 98					items[idx] = s
 99					break
100				}
101			}
102			return i, i.list.SetItems(items)
103		}
104
105	case tea.KeyMsg:
106		switch {
107		case key.Matches(msg, sessionKeyMapValue.Select):
108			selected := i.list.SelectedItem()
109			if selected == nil {
110				return i, nil
111			}
112			return i, util.CmdHandler(SelectedSessionMsg{selected.(listItem).id})
113		}
114	}
115	if i.focused {
116		u, cmd := i.list.Update(msg)
117		i.list = u
118		return i, cmd
119	}
120	return i, nil
121}
122
123func (i *sessionsCmp) View() string {
124	return i.list.View()
125}
126
127func (i *sessionsCmp) Blur() tea.Cmd {
128	i.focused = false
129	return nil
130}
131
132func (i *sessionsCmp) Focus() tea.Cmd {
133	i.focused = true
134	return nil
135}
136
137func (i *sessionsCmp) GetSize() (int, int) {
138	return i.list.Width(), i.list.Height()
139}
140
141func (i *sessionsCmp) IsFocused() bool {
142	return i.focused
143}
144
145func (i *sessionsCmp) SetSize(width int, height int) {
146	i.list.SetSize(width, height)
147}
148
149func (i *sessionsCmp) BorderText() map[layout.BorderPosition]string {
150	totalCount := len(i.list.Items())
151	itemsPerPage := i.list.Paginator.PerPage
152	currentPage := i.list.Paginator.Page
153
154	current := min(currentPage*itemsPerPage+itemsPerPage, totalCount)
155
156	pageInfo := fmt.Sprintf(
157		"%d-%d of %d",
158		currentPage*itemsPerPage+1,
159		current,
160		totalCount,
161	)
162	return map[layout.BorderPosition]string{
163		layout.TopMiddleBorder:    "Sessions",
164		layout.BottomMiddleBorder: pageInfo,
165	}
166}
167
168func (i *sessionsCmp) BindingKeys() []key.Binding {
169	return append(layout.KeyMapToSlice(i.list.KeyMap), sessionKeyMapValue.Select)
170}
171
172func NewSessionsCmp(app *app.App) SessionsCmp {
173	listDelegate := list.NewDefaultDelegate()
174	defaultItemStyle := list.NewDefaultItemStyles()
175	defaultItemStyle.SelectedTitle = defaultItemStyle.SelectedTitle.BorderForeground(styles.Secondary).Foreground(styles.Primary)
176	defaultItemStyle.SelectedDesc = defaultItemStyle.SelectedDesc.BorderForeground(styles.Secondary).Foreground(styles.Primary)
177
178	defaultStyle := list.DefaultStyles()
179	defaultStyle.FilterPrompt = defaultStyle.FilterPrompt.Foreground(styles.Secondary)
180	defaultStyle.FilterCursor = defaultStyle.FilterCursor.Foreground(styles.Flamingo)
181
182	listDelegate.Styles = defaultItemStyle
183
184	listComponent := list.New([]list.Item{}, listDelegate, 0, 0)
185	listComponent.FilterInput.PromptStyle = defaultStyle.FilterPrompt
186	listComponent.FilterInput.Cursor.Style = defaultStyle.FilterCursor
187	listComponent.SetShowTitle(false)
188	listComponent.SetShowPagination(false)
189	listComponent.SetShowHelp(false)
190	listComponent.SetShowStatusBar(false)
191	listComponent.DisableQuitKeybindings()
192
193	return &sessionsCmp{
194		app:     app,
195		list:    listComponent,
196		focused: false,
197	}
198}