99 "path/filepath"
1010 "sort"
1111 "strings"
12+ "sync"
1213 "time"
1314 "unicode/utf8"
1415
@@ -132,6 +133,9 @@ type Model struct {
132133 lastUpdateHeight int
133134 renderer * glamour.TermRenderer
134135 renderWrapWidth int
136+ rendererCache map [int ]* glamour.TermRenderer // Cache renderers by width
137+ rendererInitInFlight bool // Track if async init is running
138+ rendererInitMutex sync.Mutex // Protect cache and flag
135139 contextFile string
136140 suggestions []string
137141 selectedSuggIndex int
@@ -200,6 +204,13 @@ type ProcessingStatusMsg struct {
200204 Status string
201205}
202206
207+ // RendererReadyMsg is sent when async renderer creation completes
208+ type RendererReadyMsg struct {
209+ Renderer * glamour.TermRenderer
210+ Width int
211+ Err error
212+ }
213+
203214func New (currentModel , contextFile string , disableAnimations bool ) * Model {
204215 ta := textarea .New ()
205216 ta .Placeholder = "Type your prompt here... (@ for files, Shift+Enter (or Alt+Enter) for newline, Ctrl+X for commands, Ctrl+B to background shell, Ctrl+D or Ctrl+C×2 to quit)"
@@ -229,13 +240,20 @@ func New(currentModel, contextFile string, disableAnimations bool) *Model {
229240 spinner .WithStyle (statusStyle .MarginLeft (0 )),
230241 )
231242
243+ // Initialize renderer cache
244+ rendererCache := make (map [int ]* glamour.TermRenderer )
245+ if renderer != nil {
246+ rendererCache [80 ] = renderer
247+ }
248+
232249 m := & Model {
233250 textarea : ta ,
234251 viewport : vp ,
235252 messages : []message {},
236253 currentModel : currentModel ,
237254 contextFile : contextFile ,
238255 renderer : renderer ,
256+ rendererCache : rendererCache ,
239257 spinner : sp ,
240258 animationsDisabled : disableAnimations ,
241259 contextFreePercent : 100 ,
@@ -310,6 +328,21 @@ func (m *Model) scheduleViewportRefresh() tea.Cmd {
310328 })
311329}
312330
331+ func (m * Model ) createRendererAsync (wrapWidth int ) tea.Cmd {
332+ return func () tea.Msg {
333+ renderer , err := glamour .NewTermRenderer (
334+ glamour .WithAutoStyle (),
335+ glamour .WithWordWrap (wrapWidth ),
336+ glamour .WithPreservedNewLines (),
337+ )
338+ return RendererReadyMsg {
339+ Renderer : renderer ,
340+ Width : wrapWidth ,
341+ Err : err ,
342+ }
343+ }
344+ }
345+
313346func (m * Model ) Init () tea.Cmd {
314347 initialWindowSize := func () tea.Msg {
315348 fd := int (os .Stdout .Fd ())
@@ -331,9 +364,9 @@ func (m *Model) Init() tea.Cmd {
331364 )
332365}
333366
334- func (m * Model ) applyWindowSize (width , height int ) (bool , bool ) {
367+ func (m * Model ) applyWindowSize (width , height int ) (bool , bool , tea. Cmd ) {
335368 if width <= 0 || height <= 0 {
336- return false , false
369+ return false , false , nil
337370 }
338371
339372 widthChanged := ! m .ready || width != m .width
@@ -384,22 +417,34 @@ func (m *Model) applyWindowSize(width, height int) (bool, bool) {
384417 wrapWidth = 10
385418 }
386419
387- if wrapWidth != m .renderWrapWidth || m .renderer == nil {
388- if renderer , err := glamour .NewTermRenderer (
389- glamour .WithAutoStyle (),
390- glamour .WithWordWrap (wrapWidth ),
391- glamour .WithPreservedNewLines (),
392- ); err == nil {
393- m .renderer = renderer
420+ // Check if we need a different renderer
421+ var rendererCmd tea.Cmd
422+ needsNewRenderer := wrapWidth != m .renderWrapWidth || m .renderer == nil
423+
424+ if needsNewRenderer {
425+ m .rendererInitMutex .Lock ()
426+
427+ // Check cache first
428+ if cachedRenderer , exists := m .rendererCache [wrapWidth ]; exists {
429+ m .renderer = cachedRenderer
394430 m .renderWrapWidth = wrapWidth
431+ m .rendererInitMutex .Unlock ()
432+ } else if ! m .rendererInitInFlight {
433+ // Mark that initialization is in flight and create renderer async
434+ m .rendererInitInFlight = true
435+ m .rendererInitMutex .Unlock ()
436+ rendererCmd = m .createRendererAsync (wrapWidth )
437+ } else {
438+ // Renderer init already in flight, don't start another
439+ m .rendererInitMutex .Unlock ()
395440 }
396441 }
397442
398443 if ! m .ready {
399444 m .ready = true
400445 }
401446
402- return widthChanged , heightChanged
447+ return widthChanged , heightChanged , rendererCmd
403448}
404449
405450type ansiSeqMode int
@@ -1124,18 +1169,26 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
11241169 return m , baseCmd
11251170
11261171 case tea.WindowSizeMsg :
1127- widthChanged , heightChanged := m .applyWindowSize (msg .Width , msg .Height )
1172+ widthChanged , heightChanged , rendererCmd := m .applyWindowSize (msg .Width , msg .Height )
11281173 if ! widthChanged && heightChanged && m .viewport .AtBottom () {
11291174 m .viewport .GotoBottom ()
11301175 }
11311176
1177+ var extraCmds []tea.Cmd
1178+ if rendererCmd != nil {
1179+ extraCmds = append (extraCmds , rendererCmd )
1180+ }
1181+
11321182 if widthChanged || wasReady != m .ready {
11331183 m .viewportDirty = true
11341184 if cmd := m .scheduleViewportRefresh (); cmd != nil {
1135- return m , tea . Batch ( baseCmd , cmd )
1185+ extraCmds = append ( extraCmds , cmd )
11361186 }
11371187 }
11381188
1189+ if len (extraCmds ) > 0 {
1190+ return m , tea .Batch (append ([]tea.Cmd {baseCmd }, extraCmds ... )... )
1191+ }
11391192 return m , baseCmd
11401193
11411194 case viewportRefreshMsg :
@@ -1147,6 +1200,28 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
11471200 }
11481201 return m , baseCmd
11491202
1203+ case RendererReadyMsg :
1204+ m .rendererInitMutex .Lock ()
1205+ if msg .Err == nil && msg .Renderer != nil {
1206+ // Cache the new renderer
1207+ m .rendererCache [msg .Width ] = msg .Renderer
1208+
1209+ // If this is the current width, update active renderer
1210+ if msg .Width == m .renderWrapWidth || m .renderer == nil {
1211+ m .renderer = msg .Renderer
1212+ m .renderWrapWidth = msg .Width
1213+ m .viewportDirty = true
1214+ }
1215+ }
1216+ m .rendererInitInFlight = false
1217+ m .rendererInitMutex .Unlock ()
1218+
1219+ // Refresh viewport if needed
1220+ if m .viewportDirty {
1221+ return m , tea .Batch (baseCmd , m .scheduleViewportRefresh ())
1222+ }
1223+ return m , baseCmd
1224+
11501225 case GeneratingMsg :
11511226 if msg .Content != "" {
11521227 // If this is the first chunk of assistant content, summarize any previous tool results
0 commit comments