@@ -22,6 +22,7 @@ import (
2222 "github.com/charmbracelet/lipgloss"
2323
2424 hawkconfig "github.com/GrayCodeAI/hawk/config"
25+ "github.com/GrayCodeAI/hawk/convodag"
2526 "github.com/GrayCodeAI/hawk/engine"
2627 "github.com/GrayCodeAI/hawk/logger"
2728 "github.com/GrayCodeAI/hawk/memory"
@@ -209,6 +210,14 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
209210 return chatModel {}, err
210211 }
211212
213+ // Initialize conversation DAG for branching support
214+ if home , err := os .UserHomeDir (); err == nil {
215+ dagPath := filepath .Join (home , ".hawk" , "sessions" , "convo.db" )
216+ if dag , err := convodag .New (dagPath , sid ); err == nil {
217+ sess .ConvoDAG = dag
218+ }
219+ }
220+
212221 initWidth := 80
213222 initHeight := 24
214223 if w , h , err := term .GetSize (int (os .Stdout .Fd ())); err == nil && w > 0 {
@@ -223,7 +232,7 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
223232 vpHeight = 4
224233 }
225234 vp := viewport .New (initWidth , vpHeight )
226- vp .MouseWheelEnabled = false
235+ vp .MouseWheelEnabled = true
227236
228237 m := chatModel {input : ta , configInput : ci , spinner : sp , viewport : vp , session : sess , registry : registry , settings : settings , ref : ref , sessionID : sid , partial : & strings.Builder {}, spinnerVerb : spinnerVerbs [rand .Intn (len (spinnerVerbs ))], width : initWidth , height : initHeight , historyIdx : 0 , autoScroll : true , startedAt : time .Now (), activeSkills : make (map [string ]plugin.SmartSkill )}
229238
@@ -256,6 +265,16 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
256265 }
257266 }()
258267
268+ // Prefetch models for current provider in background so /config and /model are instant
269+ go func () {
270+ provider := effectiveProvider
271+ models , _ := hawkconfig .FetchModelsForProvider (provider )
272+ ids := extractModelIDs (models )
273+ if len (ids ) > 0 {
274+ modelCache [provider ] = ids
275+ }
276+ }()
277+
259278 // Initialize plugin runtime
260279 pr := plugin .NewRuntime ()
261280 _ = pr .LoadAll ()
@@ -319,6 +338,8 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
319338 m .messages = append (m .messages , displayMsg {role : "system" , content : "✓ Always allowed: " + m .permReq .ToolName })
320339 m .permReq = nil
321340 }
341+ m .viewDirty = true
342+ m .updateViewportContent ()
322343 return m , nil
323344 }
324345 // AskUser prompt active — Enter submits answer
@@ -329,6 +350,8 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
329350 m .messages = append (m .messages , displayMsg {role : "user" , content : answer })
330351 m .askReq .response <- answer
331352 m .askReq = nil
353+ m .viewDirty = true
354+ m .updateViewportContent ()
332355 return m , nil
333356 }
334357 // Let textarea handle other keys
@@ -349,6 +372,8 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
349372 }
350373 m .waiting = false
351374 m .input .Focus ()
375+ m .viewDirty = true
376+ m .updateViewportContent ()
352377 return m , nil
353378 }
354379 m .saveSession ()
@@ -367,6 +392,8 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
367392 m .waiting = false
368393 m .input .Focus ()
369394 }
395+ m .viewDirty = true
396+ m .updateViewportContent ()
370397 return m , nil
371398 }
372399 // Allow typing in input while streaming
@@ -384,9 +411,13 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
384411 }
385412 m .lastCtrlC = time .Now ()
386413 m .messages = append (m .messages , displayMsg {role : "system" , content : "Press Ctrl+C again to quit." })
414+ m .viewDirty = true
415+ m .updateViewportContent ()
387416 return m , nil
388417 default :
389418 next , cmd := m .handleConfigKey (msg )
419+ next .viewDirty = true
420+ next .updateViewportContent ()
390421 return next , cmd
391422 }
392423 }
@@ -405,6 +436,8 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
405436 m .session .SetModel (models [idx ])
406437 m .messages = append (m .messages , displayMsg {role : "system" , content : fmt .Sprintf ("Model → %s" , models [idx ])})
407438 }
439+ m .viewDirty = true
440+ m .updateViewportContent ()
408441 return m , nil
409442 case tea .KeyCtrlL :
410443 modes := []string {"default" , "acceptEdits" , "bypassPermissions" }
@@ -419,6 +452,8 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
419452 m .session .SetPermissionMode (modes [idx ])
420453 labels := map [string ]string {"default" : "Off" , "acceptEdits" : "Auto-edit" , "bypassPermissions" : "Full Auto" }
421454 m .messages = append (m .messages , displayMsg {role : "system" , content : fmt .Sprintf ("Autonomy → %s" , labels [modes [idx ]])})
455+ m .viewDirty = true
456+ m .updateViewportContent ()
422457 return m , nil
423458 case tea .KeyCtrlC :
424459 if time .Since (m .lastCtrlC ) < 1 * time .Second {
@@ -428,6 +463,8 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
428463 }
429464 m .lastCtrlC = time .Now ()
430465 m .messages = append (m .messages , displayMsg {role : "system" , content : "Press Ctrl+C again to quit." })
466+ m .viewDirty = true
467+ m .updateViewportContent ()
431468 return m , nil
432469 case tea .KeyTab :
433470 sugs := slashSuggestions (m .input .Value ())
@@ -499,7 +536,10 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
499536 m .historyDraft = ""
500537 m .input .Reset ()
501538 if strings .HasPrefix (text , "/" ) {
502- return m .handleCommand (text )
539+ result , cmd := m .handleCommand (text )
540+ m .viewDirty = true
541+ m .updateViewportContent ()
542+ return result , cmd
503543 }
504544 // Shell escape: !command runs directly without AI
505545 if strings .HasPrefix (text , "!" ) {
@@ -512,6 +552,7 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
512552 }
513553 m .waiting = true
514554 m .autoScroll = true
555+ m .viewDirty = true
515556 m .spinnerVerb = spinnerVerbs [rand .Intn (len (spinnerVerbs ))]
516557 m .partial .Reset ()
517558 m .startStream ()
@@ -521,12 +562,24 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
521562 case modelsFetchedMsg :
522563 if len (msg ) > 0 {
523564 m .configModels = []string (msg )
565+ // Auto-set first model so provider switch is immediately usable
566+ if m .configOpen && len (m .configModels ) > 0 {
567+ m .session .SetModel (m .configModels [0 ])
568+ _ = hawkconfig .SetGlobalSetting ("model" , m .configModels [0 ])
569+ }
570+ }
571+ if m .configOpen {
572+ m .viewDirty = true
573+ m .updateViewportContent ()
524574 }
525575 return m , nil
526576
527577 case loopTickMsg :
528578 if ! m .waiting {
529- return m .handleCommand (msg .command )
579+ result , cmd := m .handleCommand (msg .command )
580+ m .viewDirty = true
581+ m .updateViewportContent ()
582+ return result , cmd
530583 }
531584 return m , nil
532585
@@ -552,8 +605,9 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
552605
553606 case toolResultMsg :
554607 content := msg .content
555- if len (content ) > 500 {
556- content = content [:500 ] + "..."
608+ totalLen := len (content )
609+ if totalLen > 800 {
610+ content = content [:800 ] + fmt .Sprintf (" … (%d more chars)" , totalLen - 800 )
557611 }
558612 m .messages = append (m .messages , displayMsg {role : "tool_result" , content : fmt .Sprintf ("[%s] %s" , msg .name , content )})
559613 m .viewDirty = true
@@ -619,24 +673,23 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
619673 m .width = msg .Width
620674 m .height = msg .Height
621675 m .input .SetWidth (msg .Width - 4 )
622- // Resize viewport: total height minus bottom bar
623- vpHeight := msg .Height - 6
624- if vpHeight < 4 {
625- vpHeight = 4
626- }
627- m .viewport .Width = msg .Width
628- m .viewport .Height = vpHeight
629676 m .welcomeCache = buildWelcomeMessage (m .session , m .sessionID , m .registry , nil , m .settings , false , msg .Width )
630677 m .viewDirty = true
631678
632679 case spinner.TickMsg :
633680 var cmd tea.Cmd
634681 m .spinner , cmd = m .spinner .Update (msg )
635682 cmds = append (cmds , cmd )
683+ if m .waiting {
684+ m .viewDirty = true
685+ }
636686
637687 case glimmerTickMsg :
638688 m .glimmerPos ++
639689 cmds = append (cmds , glimmerTickCmd ())
690+ if m .waiting {
691+ m .viewDirty = true
692+ }
640693 }
641694
642695 if ! m .waiting {
0 commit comments