@@ -57,11 +57,26 @@ type chatModel struct {
5757 stopCh chan struct {}
5858 agent * Agent
5959 flashFrames int
60+
61+ // Glue + notifications
62+ glue * GlueClient
63+ notify * notifyManager
64+ title string // session title from glue
65+ suggestions []string // follow-up suggestions
66+ recentActions []string // recent tool actions for narration
67+ lastNarration time.Time
6068}
6169
6270// Messages
6371type agentEventMsg AgentEvent
6472type flashTickMsg struct {}
73+ type narrateTickMsg struct {}
74+ type notifyTickMsg struct {}
75+ type glueResultMsg struct {
76+ kind string // "chat", "clarify", "title", "celebrate", "suggest"
77+ text string
78+ suggestions []string
79+ }
6580
6681func newChatModel (cfg * Config ) chatModel {
6782 ti := textinput .New ()
@@ -91,11 +106,17 @@ func newChatModel(cfg *Config) chatModel {
91106 state : chatIdle ,
92107 segments : welcome ,
93108 streaming : & strings.Builder {},
109+ glue : NewGlueClient (cfg ),
110+ notify : newNotifyManager (),
94111 }
95112}
96113
97114func (m chatModel ) Init () tea.Cmd {
98- return tea .Batch (textinput .Blink , m .spinner .Tick )
115+ return tea .Batch (
116+ textinput .Blink ,
117+ m .spinner .Tick ,
118+ tea .Tick (100 * time .Millisecond , func (t time.Time ) tea.Msg { return notifyTickMsg {} }),
119+ )
99120}
100121
101122func (m chatModel ) waitForEvent () tea.Cmd {
@@ -147,13 +168,134 @@ func (m chatModel) Update(msg tea.Msg) (chatModel, tea.Cmd) {
147168 return m , nil
148169 }
149170 m .input .SetValue ("" )
150- m .startAgent (prompt )
151- cmds = append (cmds , m .waitForEvent ())
171+ m .suggestions = nil // clear old suggestions
172+
173+ // Route through glue intent classification
174+ hasHistory := m .agent != nil
175+ intent := m .glue .ClassifyIntent (prompt , hasHistory )
176+
177+ switch intent {
178+ case IntentChat :
179+ // Show user message
180+ m .segments = append (m .segments , segment {
181+ kind : "user" ,
182+ text : "\n " + styleUserLabel .Render (" ❯ " ) + prompt + "\n \n " ,
183+ })
184+ // Get reply in background
185+ glue := m .glue
186+ cmds = append (cmds , func () tea.Msg {
187+ reply := glue .ChatReply (prompt , nil )
188+ return glueResultMsg {kind : "chat" , text : reply }
189+ })
190+
191+ case IntentClarify :
192+ m .segments = append (m .segments , segment {
193+ kind : "user" ,
194+ text : "\n " + styleUserLabel .Render (" ❯ " ) + prompt + "\n \n " ,
195+ })
196+ glue := m .glue
197+ cmds = append (cmds , func () tea.Msg {
198+ reply := glue .ClarifyReply (prompt )
199+ return glueResultMsg {kind : "clarify" , text : reply }
200+ })
201+
202+ default : // IntentAgent
203+ m .startAgent (prompt )
204+ cmds = append (cmds , m .waitForEvent ())
205+ // Start narration ticker
206+ cmds = append (cmds , tea .Tick (10 * time .Second , func (t time.Time ) tea.Msg {
207+ return narrateTickMsg {}
208+ }))
209+ // Generate title in background (first prompt only)
210+ if m .title == "" {
211+ glue := m .glue
212+ cmds = append (cmds , func () tea.Msg {
213+ title := glue .GenerateTitle (prompt )
214+ return glueResultMsg {kind : "title" , text : title }
215+ })
216+ }
217+ m .notify .Push (Notification {
218+ Type : NotifyInfo ,
219+ Text : "Starting agent..." ,
220+ })
221+ }
152222 }
153223
154224 case agentEventMsg :
155225 cmds = append (cmds , m .handleAgentEvent (AgentEvent (msg )))
156226
227+ case glueResultMsg :
228+ switch msg .kind {
229+ case "chat" , "clarify" :
230+ m .segments = append (m .segments , segment {
231+ kind : "text" ,
232+ text : func () string {
233+ wrapped := wrapText (msg .text , m .width - 8 )
234+ var sb strings.Builder
235+ for _ , line := range strings .Split (wrapped , "\n " ) {
236+ sb .WriteString (" " + line + "\n " )
237+ }
238+ return sb .String ()
239+ }(),
240+ })
241+ m .rebuildViewport ()
242+
243+ case "title" :
244+ if msg .text != "" {
245+ m .title = msg .text
246+ }
247+
248+ case "narrate" :
249+ if msg .text != "" {
250+ m .notify .Push (Notification {
251+ Type : NotifyProgress ,
252+ Text : msg .text ,
253+ })
254+ }
255+
256+ case "celebrate" :
257+ if msg .text != "" {
258+ m .notify .Push (Notification {
259+ Type : NotifyCelebrate ,
260+ Text : msg .text ,
261+ })
262+ }
263+
264+ case "suggest" :
265+ m .suggestions = msg .suggestions
266+ m .rebuildViewport ()
267+ }
268+
269+ case narrateTickMsg :
270+ if m .state == chatStreaming && len (m .recentActions ) > 0 &&
271+ time .Since (m .lastNarration ) > 12 * time .Second {
272+ m .lastNarration = time .Now ()
273+ actions := make ([]string , len (m .recentActions ))
274+ copy (actions , m .recentActions )
275+ glue := m .glue
276+ cmds = append (cmds , func () tea.Msg {
277+ narration := glue .Narrate (actions )
278+ if narration != "" {
279+ return glueResultMsg {kind : "narrate" , text : narration }
280+ }
281+ return nil
282+ })
283+ }
284+ if m .state == chatStreaming {
285+ cmds = append (cmds , tea .Tick (5 * time .Second , func (t time.Time ) tea.Msg {
286+ return narrateTickMsg {}
287+ }))
288+ }
289+
290+ case notifyTickMsg :
291+ m .notify .Tick ()
292+ if m .notify .HasActive () {
293+ m .rebuildViewport ()
294+ }
295+ cmds = append (cmds , tea .Tick (100 * time .Millisecond , func (t time.Time ) tea.Msg {
296+ return notifyTickMsg {}
297+ }))
298+
157299 case flashTickMsg :
158300 m .flashFrames --
159301 if m .flashFrames <= 0 {
@@ -248,6 +390,8 @@ func (m *chatModel) startAgent(prompt string) {
248390 m .stopCh = make (chan struct {})
249391 m .streaming .Reset ()
250392 m .turns = 0
393+ m .recentActions = nil
394+ m .lastNarration = time .Now () // don't narrate immediately
251395
252396 m .segments = append (m .segments , segment {
253397 kind : "user" ,
@@ -309,6 +453,24 @@ func (m *chatModel) handleAgentEvent(evt AgentEvent) tea.Cmd {
309453 state : "pending" ,
310454 },
311455 })
456+ // Track for narration
457+ action := evt .Tool
458+ if evt .Args != nil {
459+ if p , ok := evt .Args ["path" ]; ok {
460+ if s , ok := p .(string ); ok {
461+ action += " " + s
462+ }
463+ }
464+ if p , ok := evt .Args ["command" ]; ok {
465+ if s , ok := p .(string ); ok {
466+ action += " " + s
467+ }
468+ }
469+ }
470+ m .recentActions = append (m .recentActions , action )
471+ if len (m .recentActions ) > 8 {
472+ m .recentActions = m .recentActions [len (m .recentActions )- 8 :]
473+ }
312474 m .rebuildViewport ()
313475 return m .waitForEvent ()
314476
@@ -335,6 +497,7 @@ func (m *chatModel) handleAgentEvent(evt AgentEvent) tea.Cmd {
335497
336498 case EventDone :
337499 m .flushStreamingText ()
500+ m .notify .ClearProgress ()
338501 m .files = 0
339502 if m .agent != nil {
340503 m .files = m .agent .FilesChanged ()
@@ -344,9 +507,26 @@ func (m *chatModel) handleAgentEvent(evt AgentEvent) tea.Cmd {
344507 m .state = chatDoneFlash
345508 m .flashFrames = 3
346509 m .rebuildViewport ()
347- return tea .Tick (500 * time .Millisecond , func (t time.Time ) tea.Msg {
348- return flashTickMsg {}
349- })
510+
511+ // Glue: celebration + follow-up suggestions (in background)
512+ summary := evt .Text
513+ files := m .files
514+ glue := m .glue
515+ celebrateCmd := func () tea.Msg {
516+ msg := glue .Celebrate (summary )
517+ return glueResultMsg {kind : "celebrate" , text : msg }
518+ }
519+ suggestCmd := func () tea.Msg {
520+ suggestions := glue .SuggestFollowUps (summary , files )
521+ return glueResultMsg {kind : "suggest" , suggestions : suggestions }
522+ }
523+
524+ return tea .Batch (
525+ tea .Tick (500 * time .Millisecond , func (t time.Time ) tea.Msg { return flashTickMsg {} }),
526+ celebrateCmd ,
527+ suggestCmd ,
528+ tea .Tick (5 * time .Second , func (t time.Time ) tea.Msg { return narrateTickMsg {} }),
529+ )
350530
351531 case EventError :
352532 m .flushStreamingText ()
@@ -404,14 +584,21 @@ func (m chatModel) View() string {
404584 statusParts = append (statusParts , m .spinner .View ()+ styleMuted .Render (" working" ))
405585 }
406586 statusRight := strings .Join (statusParts , styleDim .Render (" │ " ))
587+
407588 titleLeft := styleAccentText .Render (" codebase" )
589+ if m .title != "" {
590+ titleLeft += styleDim .Render (" · " ) + styleMuted .Render (m .title )
591+ }
408592
409593 gap := m .width - lipgloss .Width (titleLeft ) - lipgloss .Width (statusRight ) - 6
410594 if gap < 1 {
411595 gap = 1
412596 }
413597 header := titleLeft + strings .Repeat (" " , gap ) + statusRight
414598
599+ // ── Notifications ────────────────────────────────────────
600+ notifyBar := m .notify .Render (m .width )
601+
415602 // ── Body ─────────────────────────────────────────────────
416603 body := m .viewport .View ()
417604
@@ -426,7 +613,19 @@ func (m chatModel) View() string {
426613 frame = styleFrame .Width (m .width - 2 )
427614 }
428615
429- framedBody := frame .Render (header + "\n " + body )
616+ var innerContent string
617+ if notifyBar != "" {
618+ innerContent = header + "\n " + notifyBar + body
619+ } else {
620+ innerContent = header + "\n " + body
621+ }
622+ framedBody := frame .Render (innerContent )
623+
624+ // ── Suggestions ──────────────────────────────────────────
625+ suggestBar := ""
626+ if len (m .suggestions ) > 0 && m .state == chatIdle {
627+ suggestBar = renderSuggestions (m .suggestions , m .width ) + "\n "
628+ }
430629
431630 // ── Input ────────────────────────────────────────────────
432631 inputLine := " " + m .input .View ()
@@ -441,7 +640,7 @@ func (m chatModel) View() string {
441640 }
442641 inputRow := inputLine + strings .Repeat (" " , inputGap ) + hint
443642
444- return framedBody + "\n " + inputRow
643+ return framedBody + "\n " + suggestBar + inputRow
445644}
446645
447646var styleAccentText = lipgloss .NewStyle ().
0 commit comments