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