@@ -14,10 +14,15 @@ import (
1414)
1515
1616// ChatEventMsg wraps an incoming proto ChatEvent for the TUI.
17+ // The channel is carried so handleChatEvent can schedule the next read.
1718type ChatEventMsg struct {
1819 Event * pb.ChatEvent
20+ ch <- chan * pb.ChatEvent
1921}
2022
23+ // chatStreamDoneMsg signals the event channel was closed.
24+ type chatStreamDoneMsg struct {}
25+
2126type ChatModel struct {
2227 client * client.Client
2328 sessionID string
@@ -27,6 +32,7 @@ type ChatModel struct {
2732 viewport viewport.Model
2833 input components.InputModel
2934 statusBar components.StatusBar
35+ toolCalls components.ToolCallListModel
3036 messages []components.Message
3137 streaming string // current streaming response
3238 width int
@@ -48,6 +54,7 @@ func NewChat(c *client.Client, sessionID string, t theme.Theme, dark bool) ChatM
4854 viewport : vp ,
4955 input : input ,
5056 statusBar : statusBar ,
57+ toolCalls : components .NewToolCallList (),
5158 ctx : context .Background (),
5259 }
5360}
@@ -65,6 +72,13 @@ func (m ChatModel) Update(msg tea.Msg) (ChatModel, tea.Cmd) {
6572 m .height = msg .Height
6673 m .relayout ()
6774
75+ case tea.KeyPressMsg :
76+ // Cancel in-flight streaming with Escape
77+ if msg .String () == "esc" && m .cancelChat != nil {
78+ m .cancelChat ()
79+ m .cancelChat = nil
80+ }
81+
6882 case components.SubmitMsg :
6983 // Add user message and send to daemon
7084 m .messages = append (m .messages , components.Message {
@@ -76,7 +90,19 @@ func (m ChatModel) Update(msg tea.Msg) (ChatModel, tea.Cmd) {
7690 cmds = append (cmds , m .sendMessage (msg .Content ))
7791
7892 case ChatEventMsg :
79- cmds = append (cmds , m .handleChatEvent (msg .Event ))
93+ cmds = append (cmds , m .handleChatEvent (msg )... )
94+
95+ case chatStreamDoneMsg :
96+ // Stream channel closed without a Complete event
97+ if m .streaming != "" {
98+ m .messages = append (m .messages , components.Message {
99+ Role : components .RoleAssistant ,
100+ Content : m .streaming ,
101+ })
102+ m .streaming = ""
103+ m .refreshViewport ()
104+ }
105+ m .cancelChat = nil
80106 }
81107
82108 var inputCmd , vpCmd tea.Cmd
@@ -107,6 +133,11 @@ func (m *ChatModel) refreshViewport() {
107133 sb .WriteString (msg .Render (m .theme , m .width , m .dark ))
108134 sb .WriteString ("\n " )
109135 }
136+ // Render any active tool calls
137+ toolView := m .toolCalls .View (m .theme , m .width )
138+ if toolView != "" {
139+ sb .WriteString (toolView )
140+ }
110141 if m .streaming != "" {
111142 assistantMsg := components.Message {
112143 Role : components .RoleAssistant ,
@@ -124,7 +155,7 @@ func (m ChatModel) sendMessage(content string) tea.Cmd {
124155 return nil
125156 }
126157 ctx , cancel := context .WithCancel (m .ctx )
127- _ = cancel // stored for potential cancellation
158+ m . cancelChat = cancel
128159
129160 ch , err := m .client .SendMessage (ctx , m .sessionID , content )
130161 if err != nil {
@@ -135,24 +166,69 @@ func (m ChatModel) sendMessage(content string) tea.Cmd {
135166 }}
136167 }
137168
138- // Return first event; subsequent events come via streaming
169+ // Read first event and carry channel for subsequent reads
139170 event , ok := <- ch
140171 if ! ok {
141- return nil
172+ return chatStreamDoneMsg {}
173+ }
174+ return ChatEventMsg {Event : event , ch : ch }
175+ }
176+ }
177+
178+ // nextEvent returns a Cmd that reads the next event from the channel.
179+ func nextEvent (ch <- chan * pb.ChatEvent ) tea.Cmd {
180+ return func () tea.Msg {
181+ event , ok := <- ch
182+ if ! ok {
183+ return chatStreamDoneMsg {}
142184 }
143- return ChatEventMsg {Event : event }
185+ return ChatEventMsg {Event : event , ch : ch }
144186 }
145187}
146188
147- func (m ChatModel ) handleChatEvent (event * pb.ChatEvent ) tea.Cmd {
189+ func (m * ChatModel ) handleChatEvent (msg ChatEventMsg ) []tea.Cmd {
190+ event := msg .Event
148191 if event == nil {
149192 return nil
150193 }
194+
195+ var cmds []tea.Cmd
196+
151197 switch e := event .Event .(type ) {
152198 case * pb.ChatEvent_Token :
153199 m .streaming += e .Token .Content
154200 m .refreshViewport ()
155- return nil
201+
202+ case * pb.ChatEvent_ToolStart :
203+ m .toolCalls .AddCard (e .ToolStart .CallId , e .ToolStart .ToolName , e .ToolStart .ArgumentsJson )
204+ m .refreshViewport ()
205+
206+ case * pb.ChatEvent_ToolResult :
207+ m .toolCalls .UpdateResult (e .ToolResult .CallId , e .ToolResult .ResultJson , e .ToolResult .Success )
208+ m .refreshViewport ()
209+
210+ case * pb.ChatEvent_Permission :
211+ // Show permission prompt inline
212+ m .messages = append (m .messages , components.Message {
213+ Role : components .RoleTool ,
214+ Content : "Permission required: " + e .Permission .ToolName + " — " + e .Permission .Description ,
215+ })
216+ m .refreshViewport ()
217+
218+ case * pb.ChatEvent_AgentSpawned :
219+ m .messages = append (m .messages , components.Message {
220+ Role : components .RoleTool ,
221+ Content : "Agent spawned: " + e .AgentSpawned .AgentName + " (" + e .AgentSpawned .Role + ")" ,
222+ })
223+ m .refreshViewport ()
224+
225+ case * pb.ChatEvent_AgentMessage :
226+ m .messages = append (m .messages , components.Message {
227+ Role : components .RoleTool ,
228+ Content : "[" + e .AgentMessage .FromAgent + "] " + e .AgentMessage .Content ,
229+ })
230+ m .refreshViewport ()
231+
156232 case * pb.ChatEvent_Complete :
157233 if m .streaming != "" {
158234 m .messages = append (m .messages , components.Message {
@@ -162,17 +238,25 @@ func (m ChatModel) handleChatEvent(event *pb.ChatEvent) tea.Cmd {
162238 m .streaming = ""
163239 m .refreshViewport ()
164240 }
165- return nil
241+ m .cancelChat = nil
242+ return cmds // don't schedule next read — stream is done
243+
166244 case * pb.ChatEvent_Error :
167245 m .messages = append (m .messages , components.Message {
168246 Role : components .RoleTool ,
169247 Content : "error: " + e .Error .Message ,
170248 })
171249 m .streaming = ""
172250 m .refreshViewport ()
173- return nil
251+ m .cancelChat = nil
252+ return cmds // don't schedule next read — stream is done
253+ }
254+
255+ // Schedule read of next event from the channel
256+ if msg .ch != nil {
257+ cmds = append (cmds , nextEvent (msg .ch ))
174258 }
175- return nil
259+ return cmds
176260}
177261
178262func (m ChatModel ) View (t theme.Theme ) string {
0 commit comments