diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-16 20:06:23 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-21 13:42:00 +0200 |
| commit | bbfa60c787f2ec459f1689b9a650ddbec9693ed9 (patch) | |
| tree | f7f2aa31c460c8cc22ec40cc299c386277152241 /internal/tui/components/repl | |
| parent | 76b4065f17b87a63092acfd98c997bab53700b35 (diff) | |
| download | opencode-bbfa60c787f2ec459f1689b9a650ddbec9693ed9.tar.gz opencode-bbfa60c787f2ec459f1689b9a650ddbec9693ed9.zip | |
reimplement agent,provider and add file history
Diffstat (limited to 'internal/tui/components/repl')
| -rw-r--r-- | internal/tui/components/repl/editor.go | 201 | ||||
| -rw-r--r-- | internal/tui/components/repl/messages.go | 513 | ||||
| -rw-r--r-- | internal/tui/components/repl/sessions.go | 249 |
3 files changed, 0 insertions, 963 deletions
diff --git a/internal/tui/components/repl/editor.go b/internal/tui/components/repl/editor.go deleted file mode 100644 index b659775e0..000000000 --- a/internal/tui/components/repl/editor.go +++ /dev/null @@ -1,201 +0,0 @@ -package repl - -import ( - "strings" - - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/tui/layout" - "github.com/kujtimiihoxha/termai/internal/tui/styles" - "github.com/kujtimiihoxha/termai/internal/tui/util" - "github.com/kujtimiihoxha/vimtea" - "golang.org/x/net/context" -) - -type EditorCmp interface { - tea.Model - layout.Focusable - layout.Sizeable - layout.Bordered - layout.Bindings -} - -type editorCmp struct { - app *app.App - editor vimtea.Editor - editorMode vimtea.EditorMode - sessionID string - focused bool - width int - height int - cancelMessage context.CancelFunc -} - -type editorKeyMap struct { - SendMessage key.Binding - SendMessageI key.Binding - CancelMessage key.Binding - InsertMode key.Binding - NormaMode key.Binding - VisualMode key.Binding - VisualLineMode key.Binding -} - -var editorKeyMapValue = editorKeyMap{ - SendMessage: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "send message normal mode"), - ), - SendMessageI: key.NewBinding( - key.WithKeys("ctrl+s"), - key.WithHelp("ctrl+s", "send message insert mode"), - ), - CancelMessage: key.NewBinding( - key.WithKeys("ctrl+x"), - key.WithHelp("ctrl+x", "cancel current message"), - ), - InsertMode: key.NewBinding( - key.WithKeys("i"), - key.WithHelp("i", "insert mode"), - ), - NormaMode: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "normal mode"), - ), - VisualMode: key.NewBinding( - key.WithKeys("v"), - key.WithHelp("v", "visual mode"), - ), - VisualLineMode: key.NewBinding( - key.WithKeys("V"), - key.WithHelp("V", "visual line mode"), - ), -} - -func (m *editorCmp) Init() tea.Cmd { - return m.editor.Init() -} - -func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case vimtea.EditorModeMsg: - m.editorMode = msg.Mode - case SelectedSessionMsg: - if msg.SessionID != m.sessionID { - m.sessionID = msg.SessionID - } - } - if m.IsFocused() { - switch msg := msg.(type) { - case tea.KeyMsg: - switch { - case key.Matches(msg, editorKeyMapValue.SendMessage): - if m.editorMode == vimtea.ModeNormal { - return m, m.Send() - } - case key.Matches(msg, editorKeyMapValue.SendMessageI): - if m.editorMode == vimtea.ModeInsert { - return m, m.Send() - } - case key.Matches(msg, editorKeyMapValue.CancelMessage): - return m, m.Cancel() - } - } - u, cmd := m.editor.Update(msg) - m.editor = u.(vimtea.Editor) - return m, cmd - } - return m, nil -} - -func (m *editorCmp) Blur() tea.Cmd { - m.focused = false - return nil -} - -func (m *editorCmp) BorderText() map[layout.BorderPosition]string { - title := "New Message" - if m.focused { - title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title) - } - return map[layout.BorderPosition]string{ - layout.BottomLeftBorder: title, - } -} - -func (m *editorCmp) Focus() tea.Cmd { - m.focused = true - return m.editor.Tick() -} - -func (m *editorCmp) GetSize() (int, int) { - return m.width, m.height -} - -func (m *editorCmp) IsFocused() bool { - return m.focused -} - -func (m *editorCmp) SetSize(width int, height int) { - m.width = width - m.height = height - m.editor.SetSize(width, height) -} - -func (m *editorCmp) Cancel() tea.Cmd { - if m.cancelMessage == nil { - return util.ReportWarn("No message to cancel") - } - - m.cancelMessage() - m.cancelMessage = nil - return util.ReportWarn("Message cancelled") -} - -func (m *editorCmp) Send() tea.Cmd { - if m.cancelMessage != nil { - return util.ReportWarn("Assistant is still working on the previous message") - } - - messages, err := m.app.Messages.List(context.Background(), m.sessionID) - if err != nil { - return util.ReportError(err) - } - if hasUnfinishedMessages(messages) { - return util.ReportWarn("Assistant is still working on the previous message") - } - - content := strings.Join(m.editor.GetBuffer().Lines(), "\n") - if len(content) == 0 { - return util.ReportWarn("Message is empty") - } - ctx, cancel := context.WithCancel(context.Background()) - m.cancelMessage = cancel - go func() { - defer cancel() - m.app.CoderAgent.Generate(ctx, m.sessionID, content) - m.cancelMessage = nil - }() - - return m.editor.Reset() -} - -func (m *editorCmp) View() string { - return m.editor.View() -} - -func (m *editorCmp) BindingKeys() []key.Binding { - return layout.KeyMapToSlice(editorKeyMapValue) -} - -func NewEditorCmp(app *app.App) EditorCmp { - editor := vimtea.NewEditor( - vimtea.WithFileName("message.md"), - ) - return &editorCmp{ - app: app, - editor: editor, - } -} diff --git a/internal/tui/components/repl/messages.go b/internal/tui/components/repl/messages.go deleted file mode 100644 index 260be220e..000000000 --- a/internal/tui/components/repl/messages.go +++ /dev/null @@ -1,513 +0,0 @@ -package repl - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/glamour" - "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/termai/internal/app" - "github.com/kujtimiihoxha/termai/internal/llm/agent" - "github.com/kujtimiihoxha/termai/internal/lsp/protocol" - "github.com/kujtimiihoxha/termai/internal/message" - "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" -) - -type MessagesCmp interface { - tea.Model - layout.Focusable - layout.Bordered - layout.Sizeable - layout.Bindings -} - -type messagesCmp struct { - app *app.App - messages []message.Message - selectedMsgIdx int // Index of the selected message - session session.Session - viewport viewport.Model - mdRenderer *glamour.TermRenderer - width int - height int - focused bool - cachedView string -} - -func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case pubsub.Event[message.Message]: - if msg.Type == pubsub.CreatedEvent { - if msg.Payload.SessionID == m.session.ID { - m.messages = append(m.messages, msg.Payload) - m.renderView() - m.viewport.GotoBottom() - } - for _, v := range m.messages { - for _, c := range v.ToolCalls() { - // the message is being added to the session of a tool called - if c.ID == msg.Payload.SessionID { - m.renderView() - m.viewport.GotoBottom() - } - } - } - } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID { - for i, v := range m.messages { - if v.ID == msg.Payload.ID { - m.messages[i] = msg.Payload - m.renderView() - if i == len(m.messages)-1 { - m.viewport.GotoBottom() - } - break - } - } - } - case pubsub.Event[session.Session]: - if msg.Type == pubsub.UpdatedEvent && m.session.ID == msg.Payload.ID { - m.session = msg.Payload - } - case SelectedSessionMsg: - m.session, _ = m.app.Sessions.Get(context.Background(), msg.SessionID) - m.messages, _ = m.app.Messages.List(context.Background(), m.session.ID) - m.renderView() - m.viewport.GotoBottom() - } - if m.focused { - u, cmd := m.viewport.Update(msg) - m.viewport = u - return m, cmd - } - return m, nil -} - -func borderColor(role message.MessageRole) lipgloss.TerminalColor { - switch role { - case message.Assistant: - return styles.Mauve - case message.User: - return styles.Rosewater - } - return styles.Blue -} - -func borderText(msgRole message.MessageRole, currentMessage int) map[layout.BorderPosition]string { - role := "" - icon := "" - switch msgRole { - case message.Assistant: - role = "Assistant" - icon = styles.BotIcon - case message.User: - role = "User" - icon = styles.UserIcon - } - return map[layout.BorderPosition]string{ - layout.TopLeftBorder: lipgloss.NewStyle(). - Padding(0, 1). - Bold(true). - Foreground(styles.Crust). - Background(borderColor(msgRole)). - Render(fmt.Sprintf("%s %s ", role, icon)), - layout.TopRightBorder: lipgloss.NewStyle(). - Padding(0, 1). - Bold(true). - Foreground(styles.Crust). - Background(borderColor(msgRole)). - Render(fmt.Sprintf("#%d ", currentMessage)), - } -} - -func hasUnfinishedMessages(messages []message.Message) bool { - if len(messages) == 0 { - return false - } - for _, msg := range messages { - if !msg.IsFinished() { - return true - } - } - return false -} - -func (m *messagesCmp) renderMessageWithToolCall(content string, tools []message.ToolCall, futureMessages []message.Message) string { - allParts := []string{content} - - leftPaddingValue := 4 - connectorStyle := lipgloss.NewStyle(). - Foreground(styles.Peach). - Bold(true) - - toolCallStyle := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(styles.Peach). - Width(m.width-leftPaddingValue-5). - Padding(0, 1) - - toolResultStyle := lipgloss.NewStyle(). - Border(lipgloss.RoundedBorder()). - BorderForeground(styles.Green). - Width(m.width-leftPaddingValue-5). - Padding(0, 1) - - leftPadding := lipgloss.NewStyle().Padding(0, 0, 0, leftPaddingValue) - - runningStyle := lipgloss.NewStyle(). - Foreground(styles.Peach). - Bold(true) - - renderTool := func(toolCall message.ToolCall) string { - toolHeader := lipgloss.NewStyle(). - Bold(true). - Foreground(styles.Blue). - Render(fmt.Sprintf("%s %s", styles.ToolIcon, toolCall.Name)) - - var paramLines []string - var args map[string]interface{} - var paramOrder []string - - json.Unmarshal([]byte(toolCall.Input), &args) - - for key := range args { - paramOrder = append(paramOrder, key) - } - sort.Strings(paramOrder) - - for _, name := range paramOrder { - value := args[name] - paramName := lipgloss.NewStyle(). - Foreground(styles.Peach). - Bold(true). - Render(name) - - truncate := m.width - leftPaddingValue*2 - 10 - if len(fmt.Sprintf("%v", value)) > truncate { - value = fmt.Sprintf("%v", value)[:truncate] + lipgloss.NewStyle().Foreground(styles.Blue).Render("... (truncated)") - } - paramValue := fmt.Sprintf("%v", value) - paramLines = append(paramLines, fmt.Sprintf(" %s: %s", paramName, paramValue)) - } - - paramBlock := lipgloss.JoinVertical(lipgloss.Left, paramLines...) - - toolContent := lipgloss.JoinVertical(lipgloss.Left, toolHeader, paramBlock) - return toolCallStyle.Render(toolContent) - } - - findToolResult := func(toolCallID string, messages []message.Message) *message.ToolResult { - for _, msg := range messages { - if msg.Role == message.Tool { - for _, result := range msg.ToolResults() { - if result.ToolCallID == toolCallID { - return &result - } - } - } - } - return nil - } - - renderToolResult := func(result message.ToolResult) string { - resultHeader := lipgloss.NewStyle(). - Bold(true). - Foreground(styles.Green). - Render(fmt.Sprintf("%s Result", styles.CheckIcon)) - - // Use the same style for both header and border if it's an error - borderColor := styles.Green - if result.IsError { - resultHeader = lipgloss.NewStyle(). - Bold(true). - Foreground(styles.Red). - Render(fmt.Sprintf("%s Error", styles.ErrorIcon)) - borderColor = styles.Red - } - - truncate := 200 - content := result.Content - if len(content) > truncate { - content = content[:truncate] + lipgloss.NewStyle().Foreground(styles.Blue).Render("... (truncated)") - } - - resultContent := lipgloss.JoinVertical(lipgloss.Left, resultHeader, content) - return toolResultStyle.BorderForeground(borderColor).Render(resultContent) - } - - connector := connectorStyle.Render("└─> Tool Calls:") - allParts = append(allParts, connector) - - for _, toolCall := range tools { - toolOutput := renderTool(toolCall) - allParts = append(allParts, leftPadding.Render(toolOutput)) - - result := findToolResult(toolCall.ID, futureMessages) - if result != nil { - - resultOutput := renderToolResult(*result) - allParts = append(allParts, leftPadding.Render(resultOutput)) - - } else if toolCall.Name == agent.AgentToolName { - - runningIndicator := runningStyle.Render(fmt.Sprintf("%s Running...", styles.SpinnerIcon)) - allParts = append(allParts, leftPadding.Render(runningIndicator)) - taskSessionMessages, _ := m.app.Messages.List(context.Background(), toolCall.ID) - for _, msg := range taskSessionMessages { - if msg.Role == message.Assistant { - for _, toolCall := range msg.ToolCalls() { - toolHeader := lipgloss.NewStyle(). - Bold(true). - Foreground(styles.Blue). - Render(fmt.Sprintf("%s %s", styles.ToolIcon, toolCall.Name)) - - var paramLines []string - var args map[string]interface{} - var paramOrder []string - - json.Unmarshal([]byte(toolCall.Input), &args) - - for key := range args { - paramOrder = append(paramOrder, key) - } - sort.Strings(paramOrder) - - for _, name := range paramOrder { - value := args[name] - paramName := lipgloss.NewStyle(). - Foreground(styles.Peach). - Bold(true). - Render(name) - - truncate := 50 - if len(fmt.Sprintf("%v", value)) > truncate { - value = fmt.Sprintf("%v", value)[:truncate] + lipgloss.NewStyle().Foreground(styles.Blue).Render("... (truncated)") - } - paramValue := fmt.Sprintf("%v", value) - paramLines = append(paramLines, fmt.Sprintf(" %s: %s", paramName, paramValue)) - } - - paramBlock := lipgloss.JoinVertical(lipgloss.Left, paramLines...) - toolContent := lipgloss.JoinVertical(lipgloss.Left, toolHeader, paramBlock) - toolOutput := toolCallStyle.BorderForeground(styles.Teal).MaxWidth(m.width - leftPaddingValue*2 - 2).Render(toolContent) - allParts = append(allParts, lipgloss.NewStyle().Padding(0, 0, 0, leftPaddingValue*2).Render(toolOutput)) - } - } - } - - } else { - runningIndicator := runningStyle.Render(fmt.Sprintf("%s Running...", styles.SpinnerIcon)) - allParts = append(allParts, " "+runningIndicator) - } - } - - for _, msg := range futureMessages { - if msg.Content().String() != "" || msg.FinishReason() == "canceled" { - break - } - - for _, toolCall := range msg.ToolCalls() { - toolOutput := renderTool(toolCall) - allParts = append(allParts, " "+strings.ReplaceAll(toolOutput, "\n", "\n ")) - - result := findToolResult(toolCall.ID, futureMessages) - if result != nil { - resultOutput := renderToolResult(*result) - allParts = append(allParts, " "+strings.ReplaceAll(resultOutput, "\n", "\n ")) - } else { - runningIndicator := runningStyle.Render(fmt.Sprintf("%s Running...", styles.SpinnerIcon)) - allParts = append(allParts, " "+runningIndicator) - } - } - } - - return lipgloss.JoinVertical(lipgloss.Left, allParts...) -} - -func (m *messagesCmp) renderView() { - stringMessages := make([]string, 0) - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.CatppuccinMarkdownStyle()), - glamour.WithWordWrap(m.width-20), - glamour.WithEmoji(), - ) - textStyle := lipgloss.NewStyle().Width(m.width - 4) - currentMessage := 1 - displayedMsgCount := 0 // Track the actual displayed messages count - - prevMessageWasUser := false - for inx, msg := range m.messages { - content := msg.Content().String() - if content != "" || prevMessageWasUser || msg.FinishReason() == "canceled" { - if msg.ReasoningContent().String() != "" && content == "" { - content = msg.ReasoningContent().String() - } else if content == "" { - content = "..." - } - if msg.FinishReason() == "canceled" { - content, _ = r.Render(content) - content += lipgloss.NewStyle().Padding(1, 0, 0, 1).Foreground(styles.Error).Render(styles.ErrorIcon + " Canceled") - } else { - content, _ = r.Render(content) - } - - isSelected := inx == m.selectedMsgIdx - - border := lipgloss.DoubleBorder() - activeColor := borderColor(msg.Role) - - if isSelected { - activeColor = styles.Primary // Use primary color for selected message - } - - content = layout.Borderize( - textStyle.Render(content), - layout.BorderOptions{ - InactiveBorder: border, - ActiveBorder: border, - ActiveColor: activeColor, - InactiveColor: borderColor(msg.Role), - EmbeddedText: borderText(msg.Role, currentMessage), - }, - ) - if len(msg.ToolCalls()) > 0 { - content = m.renderMessageWithToolCall(content, msg.ToolCalls(), m.messages[inx+1:]) - } - stringMessages = append(stringMessages, content) - currentMessage++ - displayedMsgCount++ - } - if msg.Role == message.User && msg.Content().String() != "" { - prevMessageWasUser = true - } else { - prevMessageWasUser = false - } - } - m.viewport.SetContent(lipgloss.JoinVertical(lipgloss.Top, stringMessages...)) -} - -func (m *messagesCmp) View() string { - return lipgloss.NewStyle().Padding(1).Render(m.viewport.View()) -} - -func (m *messagesCmp) BindingKeys() []key.Binding { - keys := layout.KeyMapToSlice(m.viewport.KeyMap) - - return keys -} - -func (m *messagesCmp) Blur() tea.Cmd { - m.focused = false - return nil -} - -func (m *messagesCmp) projectDiagnostics() string { - errorDiagnostics := []protocol.Diagnostic{} - warnDiagnostics := []protocol.Diagnostic{} - hintDiagnostics := []protocol.Diagnostic{} - infoDiagnostics := []protocol.Diagnostic{} - for _, client := range m.app.LSPClients { - for _, d := range client.GetDiagnostics() { - for _, diag := range d { - switch diag.Severity { - case protocol.SeverityError: - errorDiagnostics = append(errorDiagnostics, diag) - case protocol.SeverityWarning: - warnDiagnostics = append(warnDiagnostics, diag) - case protocol.SeverityHint: - hintDiagnostics = append(hintDiagnostics, diag) - case protocol.SeverityInformation: - infoDiagnostics = append(infoDiagnostics, diag) - } - } - } - } - - if len(errorDiagnostics) == 0 && len(warnDiagnostics) == 0 && len(hintDiagnostics) == 0 && len(infoDiagnostics) == 0 { - return "No diagnostics" - } - - diagnostics := []string{} - - if len(errorDiagnostics) > 0 { - errStr := lipgloss.NewStyle().Foreground(styles.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics))) - diagnostics = append(diagnostics, errStr) - } - if len(warnDiagnostics) > 0 { - warnStr := lipgloss.NewStyle().Foreground(styles.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics))) - diagnostics = append(diagnostics, warnStr) - } - if len(hintDiagnostics) > 0 { - hintStr := lipgloss.NewStyle().Foreground(styles.Text).Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics))) - diagnostics = append(diagnostics, hintStr) - } - if len(infoDiagnostics) > 0 { - infoStr := lipgloss.NewStyle().Foreground(styles.Peach).Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics))) - diagnostics = append(diagnostics, infoStr) - } - - return strings.Join(diagnostics, " ") -} - -func (m *messagesCmp) BorderText() map[layout.BorderPosition]string { - title := m.session.Title - titleWidth := m.width / 2 - if len(title) > titleWidth { - title = title[:titleWidth] + "..." - } - if m.focused { - title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title) - } - borderTest := map[layout.BorderPosition]string{ - layout.TopLeftBorder: title, - layout.BottomRightBorder: m.projectDiagnostics(), - } - if hasUnfinishedMessages(m.messages) { - borderTest[layout.BottomLeftBorder] = lipgloss.NewStyle().Foreground(styles.Peach).Render("Thinking...") - } else { - borderTest[layout.BottomLeftBorder] = lipgloss.NewStyle().Foreground(styles.Text).Render("Sleeping " + styles.SleepIcon + " ") - } - - return borderTest -} - -func (m *messagesCmp) Focus() tea.Cmd { - m.focused = true - return nil -} - -func (m *messagesCmp) GetSize() (int, int) { - return m.width, m.height -} - -func (m *messagesCmp) IsFocused() bool { - return m.focused -} - -func (m *messagesCmp) SetSize(width int, height int) { - m.width = width - m.height = height - m.viewport.Width = width - 2 // padding - m.viewport.Height = height - 2 // padding - m.renderView() -} - -func (m *messagesCmp) Init() tea.Cmd { - return nil -} - -func NewMessagesCmp(app *app.App) MessagesCmp { - return &messagesCmp{ - app: app, - messages: []message.Message{}, - viewport: viewport.New(0, 0), - } -} diff --git a/internal/tui/components/repl/sessions.go b/internal/tui/components/repl/sessions.go deleted file mode 100644 index c83c40367..000000000 --- a/internal/tui/components/repl/sessions.go +++ /dev/null @@ -1,249 +0,0 @@ -package repl - -import ( - "context" - "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(context.Background()) - if err != nil { - return util.ReportError(err) - } - if len(existing) == 0 || existing[0].MessageCount > 0 { - newSession, err := i.app.Sessions.Create( - context.Background(), - "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, - } -} |
