@@ -2,6 +2,8 @@ package model
6 "math/rand"
8 "math/rand"
@@ -21,9 +23,12 @@ import (
21 "github.com/charmbracelet/crush/internal/config"
23 "github.com/charmbracelet/crush/internal/config"
22 "github.com/charmbracelet/crush/internal/history"
24 "github.com/charmbracelet/crush/internal/history"
23 "github.com/charmbracelet/crush/internal/message"
25 "github.com/charmbracelet/crush/internal/message"
26+ "github.com/charmbracelet/crush/internal/permission"
24 "github.com/charmbracelet/crush/internal/pubsub"
27 "github.com/charmbracelet/crush/internal/pubsub"
25 "github.com/charmbracelet/crush/internal/session"
28 "github.com/charmbracelet/crush/internal/session"
29+ "github.com/charmbracelet/crush/internal/tui/components/chat"
26 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
30 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
31+ "github.com/charmbracelet/crush/internal/tui/util"
27 "github.com/charmbracelet/crush/internal/ui/common"
32 "github.com/charmbracelet/crush/internal/ui/common"
28 "github.com/charmbracelet/crush/internal/ui/dialog"
33 "github.com/charmbracelet/crush/internal/ui/dialog"
29 "github.com/charmbracelet/crush/internal/ui/logo"
34 "github.com/charmbracelet/crush/internal/ui/logo"
@@ -98,7 +103,7 @@ type UI struct {
98 // Editor components
103 // Editor components
99 textarea textarea.Model
104 textarea textarea.Model
101− attachments []any // TODO: Implement attachments
106+ attachments []message.Attachment // TODO: Implement attachments
103 readyPlaceholder string
108 readyPlaceholder string
104 workingPlaceholder string
109 workingPlaceholder string
@@ -199,23 +204,12 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
202− // Build tool result map to link tool calls with their results
207+ if cmd := m.handleMessageEvents(msgs...); cmd != nil {
203− msgPtrs := make([]*message.Message, len(msgs))
208+ cmds = append(cmds, cmd)
204− for i := range msgs {
205− msgPtrs[i] = &msgs[i]
207− toolResultMap := BuildToolResultMap(msgPtrs)
209− // Add messages to chat with linked tool results
210− items := make([]MessageItem, 0, len(msgs)*2)
211− for _, msg := range msgPtrs {
212− items = append(items, GetMessageItems(m.com.Styles, msg, toolResultMap)...)
214−
210+ case pubsub.Event[message.Message]:
215− m.chat.SetMessages(items...)
211+ // TODO: Finish implementing me
216−
212+ cmds = append(cmds, m.handleMessageEvents(msg.Payload))
217− m.chat.ScrollToBottom()
219 case pubsub.Event[history.File]:
213 case pubsub.Event[history.File]:
220 cmds = append(cmds, m.handleFileEvent(msg.Payload))
214 cmds = append(cmds, m.handleFileEvent(msg.Payload))
221 case pubsub.Event[app.LSPEvent]:
215 case pubsub.Event[app.LSPEvent]:
@@ -343,24 +337,35 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
343 return m, tea.Batch(cmds...)
337 return m, tea.Batch(cmds...)
346−func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
340+func (m *UI) handleMessageEvents(msgs ...message.Message) tea.Cmd {
347− var cmds []tea.Cmd
341+ // Build tool result map to link tool calls with their results
342+ msgPtrs := make([]*message.Message, len(msgs))
343+ for i := range msgs {
344+ msgPtrs[i] = &msgs[i]
346+ toolResultMap := BuildToolResultMap(msgPtrs)
349− handleQuitKeys := func(msg tea.KeyPressMsg) bool {
348+ // Add messages to chat with linked tool results
350− switch {
349+ items := make([]MessageItem, 0, len(msgs)*2)
351− case key.Matches(msg, m.keyMap.Quit):
350+ for _, msg := range msgPtrs {
352− if !m.dialog.ContainsDialog(dialog.QuitID) {
351+ items = append(items, GetMessageItems(m.com.Styles, msg, toolResultMap)...)
353− m.dialog.OpenDialog(dialog.NewQuit(m.com))
352+ }
355− }
354+ if m.session == nil || m.session.ID == "" {
356− }
355+ m.chat.SetMessages(items...)
357− return false
356+ } else {
357+ m.chat.AppendMessages(items...)
359+ m.chat.ScrollToBottom()
365+func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
360 handleGlobalKeys := func(msg tea.KeyPressMsg) bool {
368 handleGlobalKeys := func(msg tea.KeyPressMsg) bool {
361− if handleQuitKeys(msg) {
365 case key.Matches(msg, m.keyMap.Help):
370 case key.Matches(msg, m.keyMap.Help):
366 m.help.ShowAll = !m.help.ShowAll
371 m.help.ShowAll = !m.help.ShowAll
@@ -386,13 +391,17 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
386 return false
391 return false
389− // Route all messages to dialog if one is open.
394+ if key.Matches(msg, m.keyMap.Quit) && !m.dialog.ContainsDialog(dialog.QuitID) {
390− if m.dialog.HasDialogs() {
391 // Always handle quit keys first
395 // Always handle quit keys first
392− if handleQuitKeys(msg) {
396+ if cmd := m.openQuitDialog(); cmd != nil {
393− return tea.Batch(cmds...)
397+ cmds = append(cmds, cmd)
400+ return tea.Batch(cmds...)
403+ // Route all messages to dialog if one is open.
404+ if m.dialog.HasDialogs() {
396 msg := m.dialog.Update(msg)
405 msg := m.dialog.Update(msg)
397 if msg == nil {
406 if msg == nil {
398 return tea.Batch(cmds...)
407 return tea.Batch(cmds...)
@@ -443,7 +452,30 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
443 case uiFocusEditor:
452 case uiFocusEditor:
445 case key.Matches(msg, m.keyMap.Editor.SendMessage):
454 case key.Matches(msg, m.keyMap.Editor.SendMessage):
446− // TODO: Implement me
455+ value := m.textarea.Value()
456+ if strings.HasSuffix(value, "\\") {
457+ // If the last character is a backslash, remove it and add a newline.
458+ m.textarea.SetValue(strings.TrimSuffix(value, "\\"))
462+ // Otherwise, send the message
465+ value = strings.TrimSpace(value)
466+ if value == "exit" || value == "quit" {
467+ return m.openQuitDialog()
470+ attachments := m.attachments
472+ if len(value) == 0 {
476+ m.randomizePlaceholders()
478+ return m.sendMessage(value, attachments)
447 case key.Matches(msg, m.keyMap.Tab):
479 case key.Matches(msg, m.keyMap.Tab):
448 m.focus = uiFocusMain
480 m.focus = uiFocusMain
449 m.textarea.Blur()
481 m.textarea.Blur()
@@ -1134,6 +1166,56 @@ func (m *UI) renderSidebarLogo(width int) {
1134 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
1166 m.sidebarLogo = renderLogo(m.com.Styles, true, width)
1169+// sendMessage sends a message with the given content and attachments.
1170+func (m *UI) sendMessage(content string, attachments []message.Attachment) tea.Cmd {
1171+ if m.session == nil {
1172+ return uiutil.ReportError(fmt.Errorf("no session selected"))
1174+ session := *m.session
1176+ if m.session.ID == "" {
1177+ newSession, err := m.com.App.Sessions.Create(context.Background(), "New Session")
1179+ return uiutil.ReportError(err)
1181+ session = newSession
1182+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
1184+ if m.com.App.AgentCoordinator == nil {
1185+ return util.ReportError(fmt.Errorf("coder agent is not initialized"))
1187+ m.chat.ScrollToBottom()
1188+ cmds = append(cmds, func() tea.Msg {
1189+ _, err := m.com.App.AgentCoordinator.Run(context.Background(), session.ID, content, attachments...)
1191+ isCancelErr := errors.Is(err, context.Canceled)
1192+ isPermissionErr := errors.Is(err, permission.ErrorPermissionDenied)
1193+ if isCancelErr || isPermissionErr {
1196+ return util.InfoMsg{
1197+ Type: util.InfoTypeError,
1203+ return tea.Batch(cmds...)
1206+// openQuitDialog opens the quit confirmation dialog.
1207+func (m *UI) openQuitDialog() tea.Cmd {
1208+ if m.dialog.ContainsDialog(dialog.QuitID) {
1210+ m.dialog.BringToFront(dialog.QuitID)
1214+ quitDialog := dialog.NewQuit(m.com)
1215+ m.dialog.OpenDialog(quitDialog)
1137// openCommandsDialog opens the commands dialog.
1219// openCommandsDialog opens the commands dialog.
1138func (m *UI) openCommandsDialog() tea.Cmd {
1220func (m *UI) openCommandsDialog() tea.Cmd {
1139 if m.dialog.ContainsDialog(dialog.CommandsID) {
1221 if m.dialog.ContainsDialog(dialog.CommandsID) {