on main help sign in
feat(ui): simplify editor and embed into main UI model
d9e6736Ayman Bagabas
internal/ui/model/editor.godeleted−235
@@ -1,235 +0,0 @@
1−package model
2−
3−import (
4− "math/rand"
5−
6− "charm.land/bubbles/v2/key"
7− "charm.land/bubbles/v2/textarea"
8− tea "charm.land/bubbletea/v2"
9− "github.com/charmbracelet/crush/internal/ui/common"
10−)
11−
12−type EditorKeyMap struct {
13− AddFile key.Binding
14− SendMessage key.Binding
15− OpenEditor key.Binding
16− Newline key.Binding
17−
18− // Attachments key maps
19− AttachmentDeleteMode key.Binding
20− Escape key.Binding
21− DeleteAllAttachments key.Binding
22−}
23−
24−func DefaultEditorKeyMap() EditorKeyMap {
25− return EditorKeyMap{
26− AddFile: key.NewBinding(
27− key.WithKeys("/"),
28− key.WithHelp("/", "add file"),
29− ),
30− SendMessage: key.NewBinding(
31− key.WithKeys("enter"),
32− key.WithHelp("enter", "send"),
33− ),
34− OpenEditor: key.NewBinding(
35− key.WithKeys("ctrl+o"),
36− key.WithHelp("ctrl+o", "open editor"),
37− ),
38− Newline: key.NewBinding(
39− key.WithKeys("shift+enter", "ctrl+j"),
40− // "ctrl+j" is a common keybinding for newline in many editors. If
41− // the terminal supports "shift+enter", we substitute the help text
42− // to reflect that.
43− key.WithHelp("ctrl+j", "newline"),
44− ),
45− AttachmentDeleteMode: key.NewBinding(
46− key.WithKeys("ctrl+r"),
47− key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
48− ),
49− Escape: key.NewBinding(
50− key.WithKeys("esc", "alt+esc"),
51− key.WithHelp("esc", "cancel delete mode"),
52− ),
53− DeleteAllAttachments: key.NewBinding(
54− key.WithKeys("r"),
55− key.WithHelp("ctrl+r+r", "delete all attachments"),
56− ),
57− }
58−}
59−
60−// EditorModel represents the editor UI model.
61−type EditorModel struct {
62− com *common.Common
63−
64− keyMap EditorKeyMap
65− textarea *textarea.Model
66−
67− attachments []any // TODO: Implement attachments
68−
69− readyPlaceholder string
70− workingPlaceholder string
71−}
72−
73−// NewEditorModel creates a new instance of EditorModel.
74−func NewEditorModel(com *common.Common) *EditorModel {
75− ta := textarea.New()
76− ta.SetStyles(com.Styles.TextArea)
77− ta.ShowLineNumbers = false
78− ta.CharLimit = -1
79− ta.SetVirtualCursor(false)
80− ta.Focus()
81− e := &EditorModel{
82− com: com,
83− keyMap: DefaultEditorKeyMap(),
84− textarea: ta,
85− }
86−
87− e.setEditorPrompt()
88− e.randomizePlaceholders()
89− e.textarea.Placeholder = e.readyPlaceholder
90−
91− return e
92−}
93−
94−// Init initializes the editor model.
95−func (m *EditorModel) Init() tea.Cmd {
96− return nil
97−}
98−
99−// Update handles updates to the editor model.
100−func (m *EditorModel) Update(msg tea.Msg) (*EditorModel, tea.Cmd) {
101− var cmds []tea.Cmd
102− var cmd tea.Cmd
103−
104− m.textarea, cmd = m.textarea.Update(msg)
105− cmds = append(cmds, cmd)
106−
107− // Textarea placeholder logic
108− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
109− m.textarea.Placeholder = m.workingPlaceholder
110− } else {
111− m.textarea.Placeholder = m.readyPlaceholder
112− }
113− if m.com.App.Permissions.SkipRequests() {
114− m.textarea.Placeholder = "Yolo mode!"
115− }
116−
117− // TODO: Add attachments
118−
119− return m, tea.Batch(cmds...)
120−}
121−
122−// View renders the editor model.
123−func (m *EditorModel) View() string {
124− return m.textarea.View()
125−}
126−
127−// ShortHelp returns the short help view for the editor model.
128−func (m *EditorModel) ShortHelp() []key.Binding {
129− k := m.keyMap
130− binds := []key.Binding{
131− k.AddFile,
132− k.SendMessage,
133− k.OpenEditor,
134− k.Newline,
135− }
136−
137− if len(m.attachments) > 0 {
138− binds = append(binds,
139− k.AttachmentDeleteMode,
140− k.DeleteAllAttachments,
141− k.Escape,
142− )
143− }
144−
145− return binds
146−}
147−
148−// FullHelp returns the full help view for the editor model.
149−func (m *EditorModel) FullHelp() [][]key.Binding {
150− return [][]key.Binding{
151− m.ShortHelp(),
152− }
153−}
154−
155−// Cursor returns the relative cursor position of the editor.
156−func (m *EditorModel) Cursor() *tea.Cursor {
157− return m.textarea.Cursor()
158−}
159−
160−// Blur implements Container.
161−func (m *EditorModel) Blur() tea.Cmd {
162− m.textarea.Blur()
163− return nil
164−}
165−
166−// Focus implements Container.
167−func (m *EditorModel) Focus() tea.Cmd {
168− return m.textarea.Focus()
169−}
170−
171−// Focused returns whether the editor is focused.
172−func (m *EditorModel) Focused() bool {
173− return m.textarea.Focused()
174−}
175−
176−// SetSize sets the size of the editor.
177−func (m *EditorModel) SetSize(width, height int) {
178− m.textarea.SetWidth(width)
179− m.textarea.SetHeight(height)
180−}
181−
182−func (m *EditorModel) setEditorPrompt() {
183− if m.com.App.Permissions.SkipRequests() {
184− m.textarea.SetPromptFunc(4, m.yoloPromptFunc)
185− return
186− }
187− m.textarea.SetPromptFunc(4, m.normalPromptFunc)
188−}
189−
190−func (m *EditorModel) normalPromptFunc(info textarea.PromptInfo) string {
191− t := m.com.Styles
192− if info.LineNumber == 0 {
193− return " > "
194− }
195− if info.Focused {
196− return t.EditorPromptNormalFocused.Render()
197− }
198− return t.EditorPromptNormalBlurred.Render()
199−}
200−
201−func (m *EditorModel) yoloPromptFunc(info textarea.PromptInfo) string {
202− t := m.com.Styles
203− if info.LineNumber == 0 {
204− if info.Focused {
205− return t.EditorPromptYoloIconFocused.Render()
206− } else {
207− return t.EditorPromptYoloIconBlurred.Render()
208− }
209− }
210− if info.Focused {
211− return t.EditorPromptYoloDotsFocused.Render()
212− }
213− return t.EditorPromptYoloDotsBlurred.Render()
214−}
215−
216−var readyPlaceholders = [...]string{
217− "Ready!",
218− "Ready...",
219− "Ready?",
220− "Ready for instructions",
221−}
222−
223−var workingPlaceholders = [...]string{
224− "Working!",
225− "Working...",
226− "Brrrrr...",
227− "Prrrrrrrr...",
228− "Processing...",
229− "Thinking...",
230−}
231−
232−func (m *EditorModel) randomizePlaceholders() {
233− m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
234− m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
235−}
internal/ui/model/keys.go+63−1
@@ -3,16 +3,32 @@ package model
3import "charm.land/bubbles/v2/key"
3import "charm.land/bubbles/v2/key"
4
4
5type KeyMap struct {
5type KeyMap struct {
6+ Editor struct {
7+ AddFile key.Binding
8+ SendMessage key.Binding
9+ OpenEditor key.Binding
10+ Newline key.Binding
11+ AddImage key.Binding
12+ MentionFile key.Binding
13+
14+ // Attachments key maps
15+ AttachmentDeleteMode key.Binding
16+ Escape key.Binding
17+ DeleteAllAttachments key.Binding
18+ }
19+
20+ // Global key maps
6 Quit key.Binding
21 Quit key.Binding
7 Help key.Binding
22 Help key.Binding
8 Commands key.Binding
23 Commands key.Binding
24+ Models key.Binding
9 Suspend key.Binding
25 Suspend key.Binding
10 Sessions key.Binding
26 Sessions key.Binding
11 Tab key.Binding
27 Tab key.Binding
12}
28}
13
29
14func DefaultKeyMap() KeyMap {
30func DefaultKeyMap() KeyMap {
15− return KeyMap{
31+ km := KeyMap{
16 Quit: key.NewBinding(
32 Quit: key.NewBinding(
17 key.WithKeys("ctrl+c"),
33 key.WithKeys("ctrl+c"),
18 key.WithHelp("ctrl+c", "quit"),
34 key.WithHelp("ctrl+c", "quit"),
@@ -25,6 +41,10 @@ func DefaultKeyMap() KeyMap {
25 key.WithKeys("ctrl+p"),
41 key.WithKeys("ctrl+p"),
26 key.WithHelp("ctrl+p", "commands"),
42 key.WithHelp("ctrl+p", "commands"),
27 ),
43 ),
44+ Models: key.NewBinding(
45+ key.WithKeys("ctrl+m", "ctrl+l"),
46+ key.WithHelp("ctrl+l", "models"),
47+ ),
28 Suspend: key.NewBinding(
48 Suspend: key.NewBinding(
29 key.WithKeys("ctrl+z"),
49 key.WithKeys("ctrl+z"),
30 key.WithHelp("ctrl+z", "suspend"),
50 key.WithHelp("ctrl+z", "suspend"),
@@ -38,4 +58,46 @@ func DefaultKeyMap() KeyMap {
38 key.WithHelp("tab", "change focus"),
58 key.WithHelp("tab", "change focus"),
39 ),
59 ),
40 }
60 }
61+
62+ km.Editor.AddFile = key.NewBinding(
63+ key.WithKeys("/"),
64+ key.WithHelp("/", "add file"),
65+ )
66+ km.Editor.SendMessage = key.NewBinding(
67+ key.WithKeys("enter"),
68+ key.WithHelp("enter", "send"),
69+ )
70+ km.Editor.OpenEditor = key.NewBinding(
71+ key.WithKeys("ctrl+o"),
72+ key.WithHelp("ctrl+o", "open editor"),
73+ )
74+ km.Editor.Newline = key.NewBinding(
75+ key.WithKeys("shift+enter", "ctrl+j"),
76+ // "ctrl+j" is a common keybinding for newline in many editors. If
77+ // the terminal supports "shift+enter", we substitute the help tex
78+ // to reflect that.
79+ key.WithHelp("ctrl+j", "newline"),
80+ )
81+ km.Editor.AddImage = key.NewBinding(
82+ key.WithKeys("ctrl+f"),
83+ key.WithHelp("ctrl+f", "add image"),
84+ )
85+ km.Editor.MentionFile = key.NewBinding(
86+ key.WithKeys("@"),
87+ key.WithHelp("@", "mention file"),
88+ )
89+ km.Editor.AttachmentDeleteMode = key.NewBinding(
90+ key.WithKeys("ctrl+r"),
91+ key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
92+ )
93+ km.Editor.Escape = key.NewBinding(
94+ key.WithKeys("esc", "alt+esc"),
95+ key.WithHelp("esc", "cancel delete mode"),
96+ )
97+ km.Editor.DeleteAllAttachments = key.NewBinding(
98+ key.WithKeys("r"),
99+ key.WithHelp("ctrl+r+r", "delete all attachments"),
100+ )
101+
102+ return km
41}
103}
internal/ui/model/ui.go+222−45
@@ -8,8 +8,10 @@ import (
8
8
9 "charm.land/bubbles/v2/help"
9 "charm.land/bubbles/v2/help"
10 "charm.land/bubbles/v2/key"
10 "charm.land/bubbles/v2/key"
11+ "charm.land/bubbles/v2/textarea"
11 tea "charm.land/bubbletea/v2"
12 tea "charm.land/bubbletea/v2"
12 "charm.land/lipgloss/v2"
13 "charm.land/lipgloss/v2"
14+ "github.com/charmbracelet/crush/internal/session"
13 "github.com/charmbracelet/crush/internal/ui/common"
15 "github.com/charmbracelet/crush/internal/ui/common"
14 "github.com/charmbracelet/crush/internal/ui/dialog"
16 "github.com/charmbracelet/crush/internal/ui/dialog"
15 uv "github.com/charmbracelet/ultraviolet"
17 uv "github.com/charmbracelet/ultraviolet"
@@ -20,20 +22,21 @@ type uiState uint8
20
22
21// Possible uiState values.
23// Possible uiState values.
22const (
24const (
23− uiChat uiState = iota
25+ uiEdit uiState = iota
24− uiEdit
26+ uiChat
25)
27)
26
28
27// UI represents the main user interface model.
29// UI represents the main user interface model.
28type UI struct {
30type UI struct {
29− com *common.Common
31+ com *common.Common
32+ sess *session.Session
30
33
31 state uiState
34 state uiState
32
35
33 keyMap KeyMap
36 keyMap KeyMap
37+ keyenh tea.KeyboardEnhancementsMsg
34
38
35 chat *ChatModel
39 chat *ChatModel
36− editor *EditorModel
37 side *SidebarModel
40 side *SidebarModel
38 dialog *dialog.Overlay
41 dialog *dialog.Overlay
39 help help.Model
42 help help.Model
@@ -47,18 +50,40 @@ type UI struct {
47 // QueryVersion instructs the TUI to query for the terminal version when it
50 // QueryVersion instructs the TUI to query for the terminal version when it
48 // starts.
51 // starts.
49 QueryVersion bool
52 QueryVersion bool
53+
54+ // Editor components
55+ textarea textarea.Model
56+
57+ attachments []any // TODO: Implement attachments
58+
59+ readyPlaceholder string
60+ workingPlaceholder string
50}
61}
51
62
52// New creates a new instance of the [UI] model.
63// New creates a new instance of the [UI] model.
53func New(com *common.Common) *UI {
64func New(com *common.Common) *UI {
54− return &UI{
65+ // Editor components
66+ ta := textarea.New()
67+ ta.SetStyles(com.Styles.TextArea)
68+ ta.ShowLineNumbers = false
69+ ta.CharLimit = -1
70+ ta.SetVirtualCursor(false)
71+ ta.Focus()
72+
73+ ui := &UI{
55− com: com,
74+ com: com,
56− dialog: dialog.NewOverlay(),
75+ dialog: dialog.NewOverlay(),
57− keyMap: DefaultKeyMap(),
76+ keyMap: DefaultKeyMap(),
58− editor: NewEditorModel(com),
59− side: NewSidebarModel(com),
77+ side: NewSidebarModel(com),
60− help: help.New(),
78+ help: help.New(),
79+ textarea: ta,
61 }
80 }
81+
82+ ui.setEditorPrompt()
83+ ui.randomizePlaceholders()
84+ ui.textarea.Placeholder = ui.readyPlaceholder
85+
86+ return ui
62}
87}
63
88
64// Init initializes the UI model.
89// Init initializes the UI model.
@@ -73,6 +98,7 @@ func (m *UI) Init() tea.Cmd {
73// Update handles updates to the UI model.
98// Update handles updates to the UI model.
74func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
99func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
75 var cmds []tea.Cmd
100 var cmds []tea.Cmd
101+ hasDialogs := m.dialog.HasDialogs()
76 switch msg := msg.(type) {
102 switch msg := msg.(type) {
77 case tea.EnvMsg:
103 case tea.EnvMsg:
78 // Is this Windows Terminal?
104 // Is this Windows Terminal?
@@ -88,18 +114,30 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
88 return m, nil
114 return m, nil
89 case tea.WindowSizeMsg:
115 case tea.WindowSizeMsg:
90 m.updateLayoutAndSize(msg.Width, msg.Height)
116 m.updateLayoutAndSize(msg.Width, msg.Height)
117+ case tea.KeyboardEnhancementsMsg:
118+ m.keyenh = msg
119+ if msg.SupportsKeyDisambiguation() {
120+ m.keyMap.Models.SetHelp("ctrl+m", "models")
121+ m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
122+ }
91 case tea.KeyPressMsg:
123 case tea.KeyPressMsg:
92− if m.dialog.HasDialogs() {
124+ if hasDialogs {
93 m.updateDialogs(msg, &cmds)
125 m.updateDialogs(msg, &cmds)
94− } else {
126+ }
127+ }
128+
129+ if !hasDialogs {
130+ // This branch only handles UI elements when there's no dialog shown.
131+ switch msg := msg.(type) {
132+ case tea.KeyPressMsg:
95 switch {
133 switch {
96 case key.Matches(msg, m.keyMap.Tab):
134 case key.Matches(msg, m.keyMap.Tab):
97 if m.state == uiChat {
135 if m.state == uiChat {
98 m.state = uiEdit
136 m.state = uiEdit
99− cmds = append(cmds, m.editor.Focus())
137+ cmds = append(cmds, m.textarea.Focus())
100 } else {
138 } else {
101 m.state = uiChat
139 m.state = uiChat
102− cmds = append(cmds, m.editor.Blur())
140+ m.textarea.Blur()
103 }
141 }
104 case key.Matches(msg, m.keyMap.Help):
142 case key.Matches(msg, m.keyMap.Help):
105 m.help.ShowAll = !m.help.ShowAll
143 m.help.ShowAll = !m.help.ShowAll
@@ -109,10 +147,31 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
109 m.dialog.AddDialog(dialog.NewQuit(m.com))
147 m.dialog.AddDialog(dialog.NewQuit(m.com))
110 return m, nil
148 return m, nil
111 }
149 }
150+ case key.Matches(msg, m.keyMap.Commands):
151+ // TODO: Implement me
152+ case key.Matches(msg, m.keyMap.Models):
153+ // TODO: Implement me
154+ case key.Matches(msg, m.keyMap.Sessions):
155+ // TODO: Implement me
112 default:
156 default:
113 m.updateFocused(msg, &cmds)
157 m.updateFocused(msg, &cmds)
114 }
158 }
115 }
159 }
160+
161+ // This logic gets triggered on any message type, but should it?
162+ switch m.state {
163+ case uiChat:
164+ case uiEdit:
165+ // Textarea placeholder logic
166+ if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
167+ m.textarea.Placeholder = m.workingPlaceholder
168+ } else {
169+ m.textarea.Placeholder = m.readyPlaceholder
170+ }
171+ if m.com.App.Permissions.SkipRequests() {
172+ m.textarea.Placeholder = "Yolo mode!"
173+ }
174+ }
116 }
175 }
117
176
118 return m, tea.Batch(cmds...)
177 return m, tea.Batch(cmds...)
@@ -126,7 +185,7 @@ func (m *UI) View() tea.View {
126 layers := []*lipgloss.Layer{}
185 layers := []*lipgloss.Layer{}
127
186
128 // Determine the help key map based on focus
187 // Determine the help key map based on focus
129− helpKeyMap := m.focusedKeyMap()
188+ var helpKeyMap help.KeyMap = m
130
189
131 // The screen areas we're working with
190 // The screen areas we're working with
132 area := m.layout.area
191 area := m.layout.area
@@ -137,11 +196,6 @@ func (m *UI) View() tea.View {
137
196
138 if m.dialog.HasDialogs() {
197 if m.dialog.HasDialogs() {
139 if dialogView := m.dialog.View(); dialogView != "" {
198 if dialogView := m.dialog.View(); dialogView != "" {
140− // If the dialog has its own help, use that instead
141− if len(m.dialog.FullHelp()) > 0 || len(m.dialog.ShortHelp()) > 0 {
142− helpKeyMap = m.dialog
143− }
144−
145 dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView)
199 dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView)
146 dialogArea := common.CenterRect(area, dialogWidth, dialogHeight)
200 dialogArea := common.CenterRect(area, dialogWidth, dialogHeight)
147 layers = append(layers,
201 layers = append(layers,
@@ -153,8 +207,8 @@ func (m *UI) View() tea.View {
153 }
207 }
154 }
208 }
155
209
156− if m.state == uiEdit && m.editor.Focused() {
210+ if m.state == uiEdit && m.textarea.Focused() {
157− cur := m.editor.Cursor()
211+ cur := m.textarea.Cursor()
158 cur.X++ // Adjust for app margins
212 cur.X++ // Adjust for app margins
159 cur.Y += editRect.Min.Y
213 cur.Y += editRect.Min.Y
160 v.Cursor = cur
214 v.Cursor = cur
@@ -171,7 +225,7 @@ func (m *UI) View() tea.View {
171 ).X(chatRect.Min.X).Y(chatRect.Min.Y),
225 ).X(chatRect.Min.X).Y(chatRect.Min.Y),
172 lipgloss.NewLayer(m.side.View()).
226 lipgloss.NewLayer(m.side.View()).
173 X(sideRect.Min.X).Y(sideRect.Min.Y),
227 X(sideRect.Min.X).Y(sideRect.Min.Y),
174− lipgloss.NewLayer(m.editor.View()).
228+ lipgloss.NewLayer(m.textarea.View()).
175 X(editRect.Min.X).Y(editRect.Min.Y),
229 X(editRect.Min.X).Y(editRect.Min.Y),
176 lipgloss.NewLayer(m.help.View(helpKeyMap)).
230 lipgloss.NewLayer(m.help.View(helpKeyMap)).
177 X(helpRect.Min.X).Y(helpRect.Min.Y),
231 X(helpRect.Min.X).Y(helpRect.Min.Y),
@@ -189,11 +243,82 @@ func (m *UI) View() tea.View {
189 return v
243 return v
190}
244}
191
245
192−func (m *UI) focusedKeyMap() help.KeyMap {
246+// ShortHelp implements [help.KeyMap].
193− if m.state == uiChat {
247+func (m *UI) ShortHelp() []key.Binding {
194− return m.chat
248+ var binds []key.Binding
249+ k := &m.keyMap
250+
251+ if m.sess == nil {
252+ // no session selected
253+ binds = append(binds,
254+ k.Commands,
255+ k.Models,
256+ k.Editor.Newline,
257+ k.Quit,
258+ k.Help,
259+ )
260+ } else {
261+ // we have a session
195 }
262 }
263+
264+ // switch m.state {
265+ // case uiChat:
266+ // case uiEdit:
267+ // binds = append(binds,
268+ // k.Editor.AddFile,
269+ // k.Editor.SendMessage,
270+ // k.Editor.OpenEditor,
271+ // k.Editor.Newline,
272+ // )
273+ //
274+ // if len(m.attachments) > 0 {
275+ // binds = append(binds,
276+ // k.Editor.AttachmentDeleteMode,
277+ // k.Editor.DeleteAllAttachments,
278+ // k.Editor.Escape,
279+ // )
280+ // }
281+ // }
282+
196− return m.editor
283+ return binds
284+}
285+
286+// FullHelp implements [help.KeyMap].
287+func (m *UI) FullHelp() [][]key.Binding {
288+ var binds [][]key.Binding
289+ k := &m.keyMap
290+ help := k.Help
291+ help.SetHelp("ctrl+g", "less")
292+
293+ if m.sess == nil {
294+ // no session selected
295+ binds = append(binds,
296+ []key.Binding{
297+ k.Commands,
298+ k.Models,
299+ k.Sessions,
300+ },
301+ []key.Binding{
302+ k.Editor.Newline,
303+ k.Editor.AddImage,
304+ k.Editor.MentionFile,
305+ k.Editor.OpenEditor,
306+ },
307+ []key.Binding{
308+ help,
309+ },
310+ )
311+ } else {
312+ // we have a session
313+ }
314+
315+ // switch m.state {
316+ // case uiChat:
317+ // case uiEdit:
318+ // binds = append(binds, m.ShortHelp())
319+ // }
320+
321+ return binds
197}
322}
198
323
199// updateDialogs updates the dialog overlay with the given message and appends
324// updateDialogs updates the dialog overlay with the given message and appends
@@ -213,7 +338,16 @@ func (m *UI) updateFocused(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
213 case uiChat:
338 case uiChat:
214 m.updateChat(msg, cmds)
339 m.updateChat(msg, cmds)
215 case uiEdit:
340 case uiEdit:
216− m.updateEditor(msg, cmds)
341+ switch {
342+ case key.Matches(msg, m.keyMap.Editor.Newline):
343+ m.textarea.InsertRune('\n')
344+ }
345+
346+ ta, cmd := m.textarea.Update(msg)
347+ m.textarea = ta
348+ if cmd != nil {
349+ *cmds = append(*cmds, cmd)
350+ }
217 }
351 }
218}
352}
219
353
@@ -227,26 +361,13 @@ func (m *UI) updateChat(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
227 }
361 }
228}
362}
229
363
230−// updateEditor updates the editor model with the given message and appends any
231−// resulting commands to the cmds slice.
232−func (m *UI) updateEditor(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
233− updatedEditor, cmd := m.editor.Update(msg)
234− m.editor = updatedEditor
235− if cmd != nil {
236− *cmds = append(*cmds, cmd)
237− }
238−}
239−
240// updateLayoutAndSize updates the layout and sub-models sizes based on the
364// updateLayoutAndSize updates the layout and sub-models sizes based on the
241// given terminal width and height given in cells.
365// given terminal width and height given in cells.
242func (m *UI) updateLayoutAndSize(w, h int) {
366func (m *UI) updateLayoutAndSize(w, h int) {
243 // The screen area we're working with
367 // The screen area we're working with
244 area := image.Rect(0, 0, w, h)
368 area := image.Rect(0, 0, w, h)
245− helpKeyMap := m.focusedKeyMap()
369+ var helpKeyMap help.KeyMap = m
246 helpHeight := 1
370 helpHeight := 1
247− if m.dialog.HasDialogs() && len(m.dialog.FullHelp()) > 0 && len(m.dialog.ShortHelp()) > 0 {
248− helpKeyMap = m.dialog
249− }
250 if m.help.ShowAll {
371 if m.help.ShowAll {
251 for _, row := range helpKeyMap.FullHelp() {
372 for _, row := range helpKeyMap.FullHelp() {
252 helpHeight = max(helpHeight, len(row))
373 helpHeight = max(helpHeight, len(row))
@@ -280,8 +401,9 @@ func (m *UI) updateLayoutAndSize(w, h int) {
280
401
281 // Update sub-model sizes
402 // Update sub-model sizes
282 m.side.SetWidth(m.layout.sidebar.Dx())
403 m.side.SetWidth(m.layout.sidebar.Dx())
283− m.editor.SetSize(m.layout.editor.Dx(), m.layout.editor.Dy())
404+ m.textarea.SetWidth(m.layout.editor.Dx())
405+ m.textarea.SetHeight(m.layout.editor.Dy())
284− m.help.Width = m.layout.help.Dx()
406+ m.help.SetWidth(m.layout.help.Dx())
285}
407}
286
408
287// layout defines the positioning of UI elements.
409// layout defines the positioning of UI elements.
@@ -304,3 +426,58 @@ type layout struct {
304 // help is the area for the help view.
426 // help is the area for the help view.
305 help uv.Rectangle
427 help uv.Rectangle
306}
428}
429+
430+func (m *UI) setEditorPrompt() {
431+ if m.com.App.Permissions.SkipRequests() {
432+ m.textarea.SetPromptFunc(4, m.yoloPromptFunc)
433+ return
434+ }
435+ m.textarea.SetPromptFunc(4, m.normalPromptFunc)
436+}
437+
438+func (m *UI) normalPromptFunc(info textarea.PromptInfo) string {
439+ t := m.com.Styles
440+ if info.LineNumber == 0 {
441+ return " > "
442+ }
443+ if info.Focused {
444+ return t.EditorPromptNormalFocused.Render()
445+ }
446+ return t.EditorPromptNormalBlurred.Render()
447+}
448+
449+func (m *UI) yoloPromptFunc(info textarea.PromptInfo) string {
450+ t := m.com.Styles
451+ if info.LineNumber == 0 {
452+ if info.Focused {
453+ return t.EditorPromptYoloIconFocused.Render()
454+ } else {
455+ return t.EditorPromptYoloIconBlurred.Render()
456+ }
457+ }
458+ if info.Focused {
459+ return t.EditorPromptYoloDotsFocused.Render()
460+ }
461+ return t.EditorPromptYoloDotsBlurred.Render()
462+}
463+
464+var readyPlaceholders = [...]string{
465+ "Ready!",
466+ "Ready...",
467+ "Ready?",
468+ "Ready for instructions",
469+}
470+
471+var workingPlaceholders = [...]string{
472+ "Working!",
473+ "Working...",
474+ "Brrrrr...",
475+ "Prrrrrrrr...",
476+ "Processing...",
477+ "Thinking...",
478+}
479+
480+func (m *UI) randomizePlaceholders() {
481+ m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
482+ m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
483+}