1package repl
2
3import (
4 "log"
5 "strings"
6
7 "github.com/charmbracelet/bubbles/key"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/kujtimiihoxha/termai/internal/app"
11 "github.com/kujtimiihoxha/termai/internal/llm/agent"
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 "github.com/kujtimiihoxha/vimtea"
16)
17
18type EditorCmp interface {
19 tea.Model
20 layout.Focusable
21 layout.Sizeable
22 layout.Bordered
23 layout.Bindings
24}
25
26type editorCmp struct {
27 app *app.App
28 editor vimtea.Editor
29 editorMode vimtea.EditorMode
30 sessionID string
31 focused bool
32 width int
33 height int
34}
35
36type editorKeyMap struct {
37 SendMessage key.Binding
38 SendMessageI key.Binding
39 InsertMode key.Binding
40 NormaMode key.Binding
41 VisualMode key.Binding
42 VisualLineMode key.Binding
43}
44
45var editorKeyMapValue = editorKeyMap{
46 SendMessage: key.NewBinding(
47 key.WithKeys("enter"),
48 key.WithHelp("enter", "send message normal mode"),
49 ),
50 SendMessageI: key.NewBinding(
51 key.WithKeys("ctrl+s"),
52 key.WithHelp("ctrl+s", "send message insert mode"),
53 ),
54 InsertMode: key.NewBinding(
55 key.WithKeys("i"),
56 key.WithHelp("i", "insert mode"),
57 ),
58 NormaMode: key.NewBinding(
59 key.WithKeys("esc"),
60 key.WithHelp("esc", "normal mode"),
61 ),
62 VisualMode: key.NewBinding(
63 key.WithKeys("v"),
64 key.WithHelp("v", "visual mode"),
65 ),
66 VisualLineMode: key.NewBinding(
67 key.WithKeys("V"),
68 key.WithHelp("V", "visual line mode"),
69 ),
70}
71
72func (m *editorCmp) Init() tea.Cmd {
73 return m.editor.Init()
74}
75
76func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77 switch msg := msg.(type) {
78 case vimtea.EditorModeMsg:
79 m.editorMode = msg.Mode
80 case SelectedSessionMsg:
81 if msg.SessionID != m.sessionID {
82 m.sessionID = msg.SessionID
83 }
84 }
85 if m.IsFocused() {
86 switch msg := msg.(type) {
87 case tea.KeyMsg:
88 switch {
89 case key.Matches(msg, editorKeyMapValue.SendMessage):
90 if m.editorMode == vimtea.ModeNormal {
91 return m, m.Send()
92 }
93 case key.Matches(msg, editorKeyMapValue.SendMessageI):
94 if m.editorMode == vimtea.ModeInsert {
95 return m, m.Send()
96 }
97 }
98 }
99 u, cmd := m.editor.Update(msg)
100 m.editor = u.(vimtea.Editor)
101 return m, cmd
102 }
103 return m, nil
104}
105
106func (m *editorCmp) Blur() tea.Cmd {
107 m.focused = false
108 return nil
109}
110
111func (m *editorCmp) BorderText() map[layout.BorderPosition]string {
112 title := "New Message"
113 if m.focused {
114 title = lipgloss.NewStyle().Foreground(styles.Primary).Render(title)
115 }
116 return map[layout.BorderPosition]string{
117 layout.BottomLeftBorder: title,
118 }
119}
120
121func (m *editorCmp) Focus() tea.Cmd {
122 m.focused = true
123 return m.editor.Tick()
124}
125
126func (m *editorCmp) GetSize() (int, int) {
127 return m.width, m.height
128}
129
130func (m *editorCmp) IsFocused() bool {
131 return m.focused
132}
133
134func (m *editorCmp) SetSize(width int, height int) {
135 m.width = width
136 m.height = height
137 m.editor.SetSize(width, height)
138}
139
140func (m *editorCmp) Send() tea.Cmd {
141 return func() tea.Msg {
142 messages, err := m.app.Messages.List(m.sessionID)
143 log.Printf("error: %v", err)
144 log.Printf("messages: %v", messages)
145
146 if err != nil {
147 return util.ReportError(err)
148 }
149 if hasUnfinishedMessages(messages) {
150 return util.ReportWarn("Assistant is still working on the previous message")
151 }
152 a, err := agent.NewCoderAgent(m.app)
153 log.Printf("error: %v", err)
154 log.Printf("agent: %v", a)
155 if err != nil {
156 return util.ReportError(err)
157 }
158
159 content := strings.Join(m.editor.GetBuffer().Lines(), "\n")
160 go a.Generate(m.sessionID, content)
161
162 return m.editor.Reset()
163 }
164}
165
166func (m *editorCmp) View() string {
167 return m.editor.View()
168}
169
170func (m *editorCmp) BindingKeys() []key.Binding {
171 return layout.KeyMapToSlice(editorKeyMapValue)
172}
173
174func NewEditorCmp(app *app.App) EditorCmp {
175 editor := vimtea.NewEditor(
176 vimtea.WithFileName("message.md"),
177 )
178 return &editorCmp{
179 app: app,
180 editor: editor,
181 }
182}