SuperCoder/internal/tui/components/repl/editor.go

208 lines
4.6 KiB
Go
Raw Normal View History

2025-03-21 18:20:28 +01:00
package repl
2025-03-23 19:19:08 +01:00
import (
2025-03-23 22:25:31 +01:00
"strings"
2025-03-23 19:19:08 +01:00
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
2025-03-25 13:04:36 +01:00
"github.com/charmbracelet/lipgloss"
2025-03-23 19:19:08 +01:00
"github.com/kujtimiihoxha/termai/internal/app"
2025-03-27 22:35:48 +01:00
"github.com/kujtimiihoxha/termai/internal/llm/agent"
2025-03-23 19:19:08 +01:00
"github.com/kujtimiihoxha/termai/internal/tui/layout"
2025-03-25 13:04:36 +01:00
"github.com/kujtimiihoxha/termai/internal/tui/styles"
2025-03-27 22:35:48 +01:00
"github.com/kujtimiihoxha/termai/internal/tui/util"
2025-03-23 19:19:08 +01:00
"github.com/kujtimiihoxha/vimtea"
2025-04-10 13:29:44 +02:00
"golang.org/x/net/context"
2025-03-23 19:19:08 +01:00
)
2025-03-21 18:20:28 +01:00
2025-03-23 19:19:08 +01:00
type EditorCmp interface {
tea.Model
layout.Focusable
layout.Sizeable
layout.Bordered
2025-03-23 22:25:31 +01:00
layout.Bindings
2025-03-23 19:19:08 +01:00
}
type editorCmp struct {
2025-04-10 13:29:44 +02:00
app *app.App
editor vimtea.Editor
editorMode vimtea.EditorMode
sessionID string
focused bool
width int
height int
cancelMessage context.CancelFunc
2025-03-23 19:19:08 +01:00
}
2025-03-23 22:25:31 +01:00
type editorKeyMap struct {
SendMessage key.Binding
SendMessageI key.Binding
2025-04-10 13:29:44 +02:00
CancelMessage key.Binding
2025-03-23 22:25:31 +01:00
InsertMode key.Binding
NormaMode key.Binding
VisualMode key.Binding
VisualLineMode key.Binding
2025-03-23 19:19:08 +01:00
}
2025-03-23 22:25:31 +01:00
var editorKeyMapValue = editorKeyMap{
2025-03-23 19:19:08 +01:00
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"),
),
2025-04-10 13:29:44 +02:00
CancelMessage: key.NewBinding(
key.WithKeys("ctrl+x"),
key.WithHelp("ctrl+x", "cancel current message"),
),
2025-03-23 22:25:31 +01:00
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"),
),
2025-03-23 19:19:08 +01:00
}
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 {
2025-03-23 22:25:31 +01:00
case key.Matches(msg, editorKeyMapValue.SendMessage):
2025-03-23 19:19:08 +01:00
if m.editorMode == vimtea.ModeNormal {
return m, m.Send()
}
2025-03-23 22:25:31 +01:00
case key.Matches(msg, editorKeyMapValue.SendMessageI):
2025-03-23 19:19:08 +01:00
if m.editorMode == vimtea.ModeInsert {
return m, m.Send()
}
2025-04-10 13:29:44 +02:00
case key.Matches(msg, editorKeyMapValue.CancelMessage):
return m, m.Cancel()
2025-03-23 19:19:08 +01:00
}
}
u, cmd := m.editor.Update(msg)
m.editor = u.(vimtea.Editor)
return m, cmd
}
return m, nil
}
2025-03-21 18:20:28 +01:00
2025-03-23 19:19:08 +01:00
func (m *editorCmp) Blur() tea.Cmd {
m.focused = false
2025-03-21 18:20:28 +01:00
return nil
}
2025-03-23 19:19:08 +01:00
func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
2025-03-25 13:04:36 +01:00
title := "New Message"
if m.focused {
title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
}
2025-03-23 19:19:08 +01:00
return map[layout.BorderPosition]string{
2025-03-27 22:35:48 +01:00
layout.BottomLeftBorder: title,
2025-03-23 19:19:08 +01:00
}
}
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)
}
2025-04-10 13:29:44 +02:00
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")
}
2025-03-23 19:19:08 +01:00
func (m *editorCmp) Send() tea.Cmd {
2025-04-11 18:54:13 +02:00
if m.cancelMessage != nil {
return util.ReportWarn("Assistant is still working on the previous message")
}
2025-03-27 22:35:48 +01:00
2025-04-11 18:54:13 +02:00
messages, err := m.app.Messages.List(m.sessionID)
if err != nil {
return util.ReportError(err)
}
if hasUnfinishedMessages(messages) {
return util.ReportWarn("Assistant is still working on the previous message")
}
a, err := agent.NewCoderAgent(m.app)
if err != nil {
return util.ReportError(err)
}
2025-03-27 22:35:48 +01:00
2025-04-11 18:54:13 +02:00
content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
if len(content) == 0 {
return util.ReportWarn("Message is empty")
2025-03-23 19:19:08 +01:00
}
2025-04-11 18:54:13 +02:00
ctx, cancel := context.WithCancel(m.app.Context)
m.cancelMessage = cancel
go func() {
defer cancel()
a.Generate(ctx, m.sessionID, content)
m.cancelMessage = nil
}()
return m.editor.Reset()
2025-03-21 18:20:28 +01:00
}
2025-03-23 19:19:08 +01:00
func (m *editorCmp) View() string {
return m.editor.View()
2025-03-21 18:20:28 +01:00
}
2025-03-23 22:25:31 +01:00
func (m *editorCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(editorKeyMapValue)
}
2025-03-23 19:19:08 +01:00
func NewEditorCmp(app *app.App) EditorCmp {
2025-03-27 22:35:48 +01:00
editor := vimtea.NewEditor(
vimtea.WithFileName("message.md"),
)
2025-03-23 19:19:08 +01:00
return &editorCmp{
2025-03-27 22:35:48 +01:00
app: app,
editor: editor,
2025-03-23 19:19:08 +01:00
}
2025-03-21 18:20:28 +01:00
}