@@ -20,6 +20,7 @@ import (
2020 "github.com/docker/cagent/pkg/tui/commands"
2121 "github.com/docker/cagent/pkg/tui/components/editor"
2222 "github.com/docker/cagent/pkg/tui/components/messages"
23+ "github.com/docker/cagent/pkg/tui/components/notification"
2324 "github.com/docker/cagent/pkg/tui/components/sidebar"
2425 "github.com/docker/cagent/pkg/tui/components/spinner"
2526 "github.com/docker/cagent/pkg/tui/core"
@@ -68,6 +69,15 @@ type Page interface {
6869 SendEditorContent () tea.Cmd
6970}
7071
72+ // queuedMessage represents a message waiting to be sent to the agent
73+ type queuedMessage struct {
74+ content string
75+ attachments map [string ]string
76+ }
77+
78+ // maxQueuedMessages is the maximum number of messages that can be queued
79+ const maxQueuedMessages = 5
80+
7181// chatPage implements Page
7282type chatPage struct {
7383 width , height int
@@ -87,6 +97,9 @@ type chatPage struct {
8797 msgCancel context.CancelFunc
8898 streamCancelled bool
8999
100+ // Message queue for enqueuing messages while agent is working
101+ messageQueue []queuedMessage
102+
90103 // Key map
91104 keyMap KeyMap
92105
@@ -315,8 +328,7 @@ func (p *chatPage) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
315328
316329 case editor.SendMsg :
317330 slog .Debug (msg .Content )
318- cmd := p .processMessage (msg )
319- return p , cmd
331+ return p .handleSendMsg (msg )
320332
321333 case messages.StreamCancelledMsg :
322334 model , cmd := p .messages .Update (msg )
@@ -329,6 +341,12 @@ func (p *chatPage) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
329341 cmds = append (cmds , p .messages .AddCancelledMessage ())
330342 }
331343 cmds = append (cmds , p .messages .ScrollToBottom ())
344+
345+ // Process next queued message after cancel (queue is preserved)
346+ if queueCmd := p .processNextQueuedMessage (); queueCmd != nil {
347+ cmds = append (cmds , queueCmd )
348+ }
349+
332350 return p , tea .Batch (cmds ... )
333351
334352 case msgtypes.InsertFileRefMsg :
@@ -342,6 +360,9 @@ func (p *chatPage) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
342360 p .messages = model .(messages.Model )
343361 return p , cmd
344362
363+ case msgtypes.ClearQueueMsg :
364+ return p .handleClearQueue ()
365+
345366 default :
346367 // Try to handle as a runtime event
347368 if handled , cmd := p .handleRuntimeEvent (msg ); handled {
@@ -590,6 +611,87 @@ func (p *chatPage) cancelStream(showCancelMessage bool) tea.Cmd {
590611 )
591612}
592613
614+ // handleSendMsg handles incoming messages from the editor, either processing
615+ // them immediately or queuing them if the agent is busy.
616+ func (p * chatPage ) handleSendMsg (msg editor.SendMsg ) (layout.Model , tea.Cmd ) {
617+ // If not working, process immediately
618+ if ! p .working {
619+ cmd := p .processMessage (msg )
620+ return p , cmd
621+ }
622+
623+ // If queue is full, reject the message
624+ if len (p .messageQueue ) >= maxQueuedMessages {
625+ return p , notification .WarningCmd (fmt .Sprintf ("Queue full (max %d messages). Please wait." , maxQueuedMessages ))
626+ }
627+
628+ // Add to queue
629+ p .messageQueue = append (p .messageQueue , queuedMessage {
630+ content : msg .Content ,
631+ attachments : msg .Attachments ,
632+ })
633+ p .syncQueueToSidebar ()
634+
635+ queueLen := len (p .messageQueue )
636+ notifyMsg := fmt .Sprintf ("Message queued (%d waiting) · Ctrl+X to clear" , queueLen )
637+
638+ return p , notification .InfoCmd (notifyMsg )
639+ }
640+
641+ // processNextQueuedMessage pops the next message from the queue and processes it.
642+ // Returns nil if the queue is empty.
643+ func (p * chatPage ) processNextQueuedMessage () tea.Cmd {
644+ if len (p .messageQueue ) == 0 {
645+ return nil
646+ }
647+
648+ // Pop the first message from the queue
649+ queued := p .messageQueue [0 ]
650+ p .messageQueue [0 ] = queuedMessage {} // zero out to allow GC
651+ p .messageQueue = p .messageQueue [1 :]
652+ p .syncQueueToSidebar ()
653+
654+ msg := editor.SendMsg {
655+ Content : queued .content ,
656+ Attachments : queued .attachments ,
657+ }
658+
659+ return p .processMessage (msg )
660+ }
661+
662+ // handleClearQueue clears all queued messages and shows a notification.
663+ func (p * chatPage ) handleClearQueue () (layout.Model , tea.Cmd ) {
664+ count := len (p .messageQueue )
665+ if count == 0 {
666+ return p , notification .InfoCmd ("No messages queued" )
667+ }
668+
669+ p .messageQueue = nil
670+ p .syncQueueToSidebar ()
671+
672+ var msg string
673+ if count == 1 {
674+ msg = "Cleared 1 queued message"
675+ } else {
676+ msg = fmt .Sprintf ("Cleared %d queued messages" , count )
677+ }
678+ return p , notification .SuccessCmd (msg )
679+ }
680+
681+ // syncQueueToSidebar updates the sidebar with truncated previews of queued messages.
682+ func (p * chatPage ) syncQueueToSidebar () {
683+ previews := make ([]string , len (p .messageQueue ))
684+ for i , qm := range p .messageQueue {
685+ // Take first line and limit length for preview
686+ content := strings .TrimSpace (qm .content )
687+ if idx := strings .IndexAny (content , "\n \r " ); idx != - 1 {
688+ content = content [:idx ]
689+ }
690+ previews [i ] = content
691+ }
692+ p .sidebar .SetQueuedMessages (previews )
693+ }
694+
593695// processMessage processes a message with the runtime
594696func (p * chatPage ) processMessage (msg editor.SendMsg ) tea.Cmd {
595697 if p .msgCancel != nil {
@@ -789,7 +891,20 @@ func (p *chatPage) renderResizeHandle(width int) string {
789891
790892 if p .working {
791893 // Truncate right side and append spinner (handle stays centered)
792- suffix := " " + p .spinner .View () + " " + styles .SpinnerDotsHighlightStyle .Render ("Working…" )
894+ workingText := "Working…"
895+ if queueLen := len (p .messageQueue ); queueLen > 0 {
896+ workingText = fmt .Sprintf ("Working… (%d queued)" , queueLen )
897+ }
898+ suffix := " " + p .spinner .View () + " " + styles .SpinnerDotsHighlightStyle .Render (workingText )
899+ suffixWidth := lipgloss .Width (suffix )
900+ truncated := lipgloss .NewStyle ().MaxWidth (width - 2 - suffixWidth ).Render (fullLine )
901+ return truncated + suffix
902+ }
903+
904+ // Show queue count even when not working (messages waiting to be processed)
905+ if queueLen := len (p .messageQueue ); queueLen > 0 {
906+ queueText := fmt .Sprintf ("%d queued" , queueLen )
907+ suffix := " " + styles .WarningStyle .Render (queueText ) + " "
793908 suffixWidth := lipgloss .Width (suffix )
794909 truncated := lipgloss .NewStyle ().MaxWidth (width - 2 - suffixWidth ).Render (fullLine )
795910 return truncated + suffix
0 commit comments