11package cmd
22
33import (
4+ "context"
45 "fmt"
56 "strings"
7+ "sync"
68
79 "github.com/charmbracelet/bubbles/spinner"
810 "github.com/charmbracelet/bubbles/textarea"
9- "github.com/charmbracelet/bubbletea"
11+ tea "github.com/charmbracelet/bubbletea"
1012 "github.com/charmbracelet/lipgloss"
13+
14+ "github.com/GrayCodeAI/hawk/engine"
15+ "github.com/GrayCodeAI/hawk/prompt"
1116)
1217
13- // Styles
1418var (
15- tealColor = lipgloss .Color ("#4ECDC4" )
16- dimColor = lipgloss .Color ("#666666" )
17- errorColor = lipgloss .Color ("#e05555" )
18- userStyle = lipgloss .NewStyle ().Foreground (tealColor ).Bold (true )
19- assistStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("#FFFFFF" ))
20- dimStyle = lipgloss .NewStyle ().Foreground (dimColor )
21- errorStyle = lipgloss .NewStyle ().Foreground (errorColor )
22- headerStyle = lipgloss .NewStyle ().Foreground (tealColor ).Bold (true )
19+ tealColor = lipgloss .Color ("#4ECDC4" )
20+ dimColor = lipgloss .Color ("#666666" )
21+ errorColor = lipgloss .Color ("#e05555" )
22+ userStyle = lipgloss .NewStyle ().Foreground (tealColor ).Bold (true )
23+ assistStyle = lipgloss .NewStyle ().Foreground (lipgloss .Color ("#FFFFFF" ))
24+ dimStyle = lipgloss .NewStyle ().Foreground (dimColor )
25+ errorStyle = lipgloss .NewStyle ().Foreground (errorColor )
26+ headerStyle = lipgloss .NewStyle ().Foreground (tealColor ).Bold (true )
2327)
2428
25- type chatMsg struct {
29+ type streamChunkMsg string
30+ type streamDoneMsg struct {}
31+ type streamErrMsg struct { err error }
32+
33+ // progRef holds a reference to the tea.Program, set after creation.
34+ type progRef struct {
35+ mu sync.Mutex
36+ p * tea.Program
37+ }
38+
39+ func (r * progRef ) Set (p * tea.Program ) { r .mu .Lock (); r .p = p ; r .mu .Unlock () }
40+ func (r * progRef ) Send (msg tea.Msg ) { r .mu .Lock (); p := r .p ; r .mu .Unlock (); if p != nil { p .Send (msg ) } }
41+
42+ type displayMsg struct {
2643 role string
2744 content string
2845}
2946
30- type errMsg struct { err error }
31-
3247type chatModel struct {
3348 input textarea.Model
3449 spinner spinner.Model
35- messages []chatMsg
50+ session * engine.Session
51+ ref * progRef
52+ messages []displayMsg
53+ partial strings.Builder
3654 waiting bool
3755 width int
3856 height int
39- err error
4057 quitting bool
4158}
4259
43- func newChatModel () chatModel {
60+ func newChatModel (ref * progRef ) chatModel {
4461 ta := textarea .New ()
4562 ta .Placeholder = "Message hawk..."
4663 ta .Focus ()
@@ -53,7 +70,9 @@ func newChatModel() chatModel {
5370 sp .Spinner = spinner .Dot
5471 sp .Style = lipgloss .NewStyle ().Foreground (tealColor )
5572
56- return chatModel {input : ta , spinner : sp }
73+ sess := engine .NewSession (provider , model , prompt .System ())
74+
75+ return chatModel {input : ta , spinner : sp , session : sess , ref : ref }
5776}
5877
5978func (m chatModel ) Init () tea.Cmd {
@@ -65,6 +84,13 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6584
6685 switch msg := msg .(type ) {
6786 case tea.KeyMsg :
87+ if m .waiting {
88+ if msg .Type == tea .KeyCtrlC {
89+ m .quitting = true
90+ return m , tea .Quit
91+ }
92+ return m , nil
93+ }
6894 switch msg .Type {
6995 case tea .KeyCtrlC :
7096 m .quitting = true
@@ -78,51 +104,91 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
78104 m .quitting = true
79105 return m , tea .Quit
80106 }
81- m .messages = append (m .messages , chatMsg {role : "user" , content : text })
107+ m .messages = append (m .messages , displayMsg {role : "user" , content : text })
108+ m .session .AddUser (text )
82109 m .input .Reset ()
83110 m .waiting = true
84- // TODO: Phase 1 — send to eyrie LLM here
85- m .messages = append (m .messages , chatMsg {role : "assistant" , content : "🚧 LLM not wired yet — eyrie integration coming in Phase 1." })
86- m .waiting = false
111+ m .partial .Reset ()
112+ m .startStream ()
87113 return m , nil
88114 }
89115
116+ case streamChunkMsg :
117+ m .partial .WriteString (string (msg ))
118+ return m , nil
119+
120+ case streamDoneMsg :
121+ content := m .partial .String ()
122+ if content != "" {
123+ m .messages = append (m .messages , displayMsg {role : "assistant" , content : content })
124+ m .session .AddAssistant (content )
125+ }
126+ m .partial .Reset ()
127+ m .waiting = false
128+ m .input .Focus ()
129+ return m , nil
130+
131+ case streamErrMsg :
132+ m .messages = append (m .messages , displayMsg {role : "error" , content : msg .err .Error ()})
133+ m .partial .Reset ()
134+ m .waiting = false
135+ m .input .Focus ()
136+ return m , nil
137+
90138 case tea.WindowSizeMsg :
91139 m .width = msg .Width
92140 m .height = msg .Height
93141 m .input .SetWidth (msg .Width - 2 )
94142
95- case errMsg :
96- m .err = msg .err
97- m .waiting = false
98- return m , nil
99-
100143 case spinner.TickMsg :
101144 var cmd tea.Cmd
102145 m .spinner , cmd = m .spinner .Update (msg )
103146 cmds = append (cmds , cmd )
104147 }
105148
106- var cmd tea.Cmd
107- m .input , cmd = m .input .Update (msg )
108- cmds = append (cmds , cmd )
149+ if ! m .waiting {
150+ var cmd tea.Cmd
151+ m .input , cmd = m .input .Update (msg )
152+ cmds = append (cmds , cmd )
153+ }
109154
110155 return m , tea .Batch (cmds ... )
111156}
112157
158+ func (m * chatModel ) startStream () {
159+ sess := m .session
160+ ref := m .ref
161+ go func () {
162+ ch , err := sess .Stream (context .Background ())
163+ if err != nil {
164+ ref .Send (streamErrMsg {err : err })
165+ return
166+ }
167+ for ev := range ch {
168+ switch ev .Type {
169+ case "content" :
170+ ref .Send (streamChunkMsg (ev .Content ))
171+ case "error" :
172+ ref .Send (streamErrMsg {err : fmt .Errorf ("%s" , ev .Content )})
173+ return
174+ }
175+ }
176+ ref .Send (streamDoneMsg {})
177+ }()
178+ }
179+
113180func (m chatModel ) View () string {
114181 if m .quitting {
115182 return dimStyle .Render ("Goodbye." ) + "\n "
116183 }
117184
118185 var b strings.Builder
119186
120- // Header
121187 b .WriteString (headerStyle .Render (fmt .Sprintf ("🦅 hawk v%s" , version )))
188+ b .WriteString (dimStyle .Render (fmt .Sprintf (" %s/%s" , m .session .Provider (), m .session .Model ())))
122189 b .WriteString (dimStyle .Render (" (ctrl+c to quit)" ))
123190 b .WriteString ("\n \n " )
124191
125- // Messages
126192 for _ , msg := range m .messages {
127193 switch msg .role {
128194 case "user" :
@@ -138,25 +204,59 @@ func (m chatModel) View() string {
138204 b .WriteString ("\n \n " )
139205 }
140206
141- // Spinner
142207 if m .waiting {
143- b .WriteString (m .spinner .View () + " Thinking...\n \n " )
208+ partial := m .partial .String ()
209+ if partial != "" {
210+ b .WriteString (assistStyle .Render ("hawk: " ))
211+ b .WriteString (partial )
212+ b .WriteString ("\n \n " )
213+ } else {
214+ b .WriteString (m .spinner .View () + " Thinking...\n \n " )
215+ }
144216 }
145217
146- // Error
147- if m . err != nil {
148- b .WriteString (errorStyle . Render ( "Error: " + m . err . Error ()) + " \n \n " )
218+ if ! m . waiting {
219+ b . WriteString ( m . input . View ())
220+ b .WriteString (" \n " )
149221 }
150222
151- // Input
152- b .WriteString (m .input .View ())
153- b .WriteString ("\n " )
154-
155223 return b .String ()
156224}
157225
158226func runChat () error {
159- p := tea .NewProgram (newChatModel (), tea .WithAltScreen ())
227+ ref := & progRef {}
228+ m := newChatModel (ref )
229+
230+ if promptFlag != "" {
231+ m .messages = append (m .messages , displayMsg {role : "user" , content : promptFlag })
232+ m .session .AddUser (promptFlag )
233+ m .waiting = true
234+ }
235+
236+ p := tea .NewProgram (m , tea .WithAltScreen ())
237+ ref .Set (p )
238+
239+ if promptFlag != "" {
240+ sess := m .session
241+ go func () {
242+ ch , err := sess .Stream (context .Background ())
243+ if err != nil {
244+ p .Send (streamErrMsg {err : err })
245+ return
246+ }
247+ for ev := range ch {
248+ switch ev .Type {
249+ case "content" :
250+ p .Send (streamChunkMsg (ev .Content ))
251+ case "error" :
252+ p .Send (streamErrMsg {err : fmt .Errorf ("%s" , ev .Content )})
253+ return
254+ }
255+ }
256+ p .Send (streamDoneMsg {})
257+ }()
258+ }
259+
160260 _ , err := p .Run ()
161261 return err
162262}
0 commit comments