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}