| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- package repl
- import (
- "fmt"
- "strings"
- "github.com/charmbracelet/bubbles/key"
- "github.com/charmbracelet/bubbles/list"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
- "github.com/kujtimiihoxha/termai/internal/app"
- "github.com/kujtimiihoxha/termai/internal/pubsub"
- "github.com/kujtimiihoxha/termai/internal/session"
- "github.com/kujtimiihoxha/termai/internal/tui/layout"
- "github.com/kujtimiihoxha/termai/internal/tui/styles"
- "github.com/kujtimiihoxha/termai/internal/tui/util"
- )
- type SessionsCmp interface {
- tea.Model
- layout.Sizeable
- layout.Focusable
- layout.Bordered
- layout.Bindings
- }
- type sessionsCmp struct {
- app *app.App
- list list.Model
- focused bool
- }
- type listItem struct {
- id, title, desc string
- }
- func (i listItem) Title() string { return i.title }
- func (i listItem) Description() string { return i.desc }
- func (i listItem) FilterValue() string { return i.title }
- type InsertSessionsMsg struct {
- sessions []session.Session
- }
- type SelectedSessionMsg struct {
- SessionID string
- }
- type sessionsKeyMap struct {
- Select key.Binding
- }
- var sessionKeyMapValue = sessionsKeyMap{
- Select: key.NewBinding(
- key.WithKeys("enter", " "),
- key.WithHelp("enter/space", "select session"),
- ),
- }
- func (i *sessionsCmp) Init() tea.Cmd {
- existing, err := i.app.Sessions.List()
- if err != nil {
- return util.ReportError(err)
- }
- if len(existing) == 0 || existing[0].MessageCount > 0 {
- newSession, err := i.app.Sessions.Create(
- "New Session",
- )
- if err != nil {
- return util.ReportError(err)
- }
- existing = append([]session.Session{newSession}, existing...)
- }
- return tea.Batch(
- util.CmdHandler(InsertSessionsMsg{existing}),
- util.CmdHandler(SelectedSessionMsg{existing[0].ID}),
- )
- }
- func (i *sessionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case InsertSessionsMsg:
- items := make([]list.Item, len(msg.sessions))
- for i, s := range msg.sessions {
- items[i] = listItem{
- id: s.ID,
- title: s.Title,
- desc: formatTokensAndCost(s.PromptTokens+s.CompletionTokens, s.Cost),
- }
- }
- return i, i.list.SetItems(items)
- case pubsub.Event[session.Session]:
- if msg.Type == pubsub.CreatedEvent && msg.Payload.ParentSessionID == "" {
- // Check if the session is already in the list
- items := i.list.Items()
- for _, item := range items {
- s := item.(listItem)
- if s.id == msg.Payload.ID {
- return i, nil
- }
- }
- // insert the new session at the top of the list
- items = append([]list.Item{listItem{
- id: msg.Payload.ID,
- title: msg.Payload.Title,
- desc: formatTokensAndCost(msg.Payload.PromptTokens+msg.Payload.CompletionTokens, msg.Payload.Cost),
- }}, items...)
- return i, i.list.SetItems(items)
- } else if msg.Type == pubsub.UpdatedEvent {
- // update the session in the list
- items := i.list.Items()
- for idx, item := range items {
- s := item.(listItem)
- if s.id == msg.Payload.ID {
- s.title = msg.Payload.Title
- s.desc = formatTokensAndCost(msg.Payload.PromptTokens+msg.Payload.CompletionTokens, msg.Payload.Cost)
- items[idx] = s
- break
- }
- }
- return i, i.list.SetItems(items)
- }
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, sessionKeyMapValue.Select):
- selected := i.list.SelectedItem()
- if selected == nil {
- return i, nil
- }
- return i, util.CmdHandler(SelectedSessionMsg{selected.(listItem).id})
- }
- }
- if i.focused {
- u, cmd := i.list.Update(msg)
- i.list = u
- return i, cmd
- }
- return i, nil
- }
- func (i *sessionsCmp) View() string {
- return i.list.View()
- }
- func (i *sessionsCmp) Blur() tea.Cmd {
- i.focused = false
- return nil
- }
- func (i *sessionsCmp) Focus() tea.Cmd {
- i.focused = true
- return nil
- }
- func (i *sessionsCmp) GetSize() (int, int) {
- return i.list.Width(), i.list.Height()
- }
- func (i *sessionsCmp) IsFocused() bool {
- return i.focused
- }
- func (i *sessionsCmp) SetSize(width int, height int) {
- i.list.SetSize(width, height)
- }
- func (i *sessionsCmp) BorderText() map[layout.BorderPosition]string {
- totalCount := len(i.list.Items())
- itemsPerPage := i.list.Paginator.PerPage
- currentPage := i.list.Paginator.Page
- current := min(currentPage*itemsPerPage+itemsPerPage, totalCount)
- pageInfo := fmt.Sprintf(
- "%d-%d of %d",
- currentPage*itemsPerPage+1,
- current,
- totalCount,
- )
- title := "Sessions"
- if i.focused {
- title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
- }
- return map[layout.BorderPosition]string{
- layout.TopMiddleBorder: title,
- layout.BottomMiddleBorder: pageInfo,
- }
- }
- func (i *sessionsCmp) BindingKeys() []key.Binding {
- return append(layout.KeyMapToSlice(i.list.KeyMap), sessionKeyMapValue.Select)
- }
- func formatTokensAndCost(tokens int64, cost float64) string {
- // Format tokens in human-readable format (e.g., 110K, 1.2M)
- var formattedTokens string
- switch {
- case tokens >= 1_000_000:
- formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
- case tokens >= 1_000:
- formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
- default:
- formattedTokens = fmt.Sprintf("%d", tokens)
- }
- // Remove .0 suffix if present
- if strings.HasSuffix(formattedTokens, ".0K") {
- formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
- }
- if strings.HasSuffix(formattedTokens, ".0M") {
- formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
- }
- // Format cost with $ symbol and 2 decimal places
- formattedCost := fmt.Sprintf("$%.2f", cost)
- return fmt.Sprintf("Tokens: %s, Cost: %s", formattedTokens, formattedCost)
- }
- func NewSessionsCmp(app *app.App) SessionsCmp {
- listDelegate := list.NewDefaultDelegate()
- defaultItemStyle := list.NewDefaultItemStyles()
- defaultItemStyle.SelectedTitle = defaultItemStyle.SelectedTitle.BorderForeground(styles.Secondary).Foreground(styles.Primary)
- defaultItemStyle.SelectedDesc = defaultItemStyle.SelectedDesc.BorderForeground(styles.Secondary).Foreground(styles.Primary)
- defaultStyle := list.DefaultStyles()
- defaultStyle.FilterPrompt = defaultStyle.FilterPrompt.Foreground(styles.Secondary)
- defaultStyle.FilterCursor = defaultStyle.FilterCursor.Foreground(styles.Flamingo)
- listDelegate.Styles = defaultItemStyle
- listComponent := list.New([]list.Item{}, listDelegate, 0, 0)
- listComponent.FilterInput.PromptStyle = defaultStyle.FilterPrompt
- listComponent.FilterInput.Cursor.Style = defaultStyle.FilterCursor
- listComponent.SetShowTitle(false)
- listComponent.SetShowPagination(false)
- listComponent.SetShowHelp(false)
- listComponent.SetShowStatusBar(false)
- listComponent.DisableQuitKeybindings()
- return &sessionsCmp{
- app: app,
- list: listComponent,
- focused: false,
- }
- }
|