@@ -129,18 +129,23 @@ type model struct {
129129 // Dynamic Commands from Extensions
130130 dynamicCommands map [string ]brain.CLICommand
131131
132- // Non-blocking Engine
133- reactor * reactor.Reactor
134- md * reactor.MarkdownRenderer
132+ // Non-blocking Engine
133+ reactor * reactor.Reactor
134+ md * reactor.MarkdownRenderer
135135 lastRenderTime time.Time
136- }
137136
138- type layoutMsg struct {
139- content string
140- wasAtBottom bool
141- wasAtTop bool
142- prevOffset int
143- }
137+ // Memoization
138+ lastViewportWidth int
139+ lastMessageCount int
140+ memoizedView string
141+ }
142+
143+ type layoutMsg struct {
144+ content string
145+ wasAtBottom bool
146+ wasAtTop bool
147+ prevOffset int
148+ }
144149type recordTickMsg time.Time
145150
146151type checkUpdateTickMsg time.Time
@@ -1191,11 +1196,31 @@ func (m *model) renderMessages() string {
11911196 // Sync renderer width with viewport
11921197 m .md .SetWidth (m .viewport .Width )
11931198
1194- // Use goroutines to render messages in parallel
1195- rendered := make ([]string , len (m .messages ))
1199+ // O(1) optimization: If width hasn't changed AND history hasn't changed, return memoized
1200+ if m .lastViewportWidth == m .viewport .Width && m .lastMessageCount == len (m .messages ) && m .memoizedView != "" {
1201+ return m .memoizedView
1202+ }
1203+
1204+ // O(1) incremental optimization: If width is SAME but messages grew, only render NEW items
1205+ isIncremental := m .lastViewportWidth == m .viewport .Width && len (m .messages ) > m .lastMessageCount && m .memoizedView != ""
1206+
1207+ startIndex := 0
1208+ var sb strings.Builder
1209+
1210+ if isIncremental {
1211+ startIndex = m .lastMessageCount
1212+ sb .WriteString (m .memoizedView )
1213+ if startIndex > 0 {
1214+ sb .WriteString ("\n \n " )
1215+ }
1216+ }
1217+
1218+ // Only iterate through what's actually new
1219+ newMessages := m .messages [startIndex :]
1220+ rendered := make ([]string , len (newMessages ))
11961221 var wg sync.WaitGroup
11971222
1198- for i , msg := range m . messages {
1223+ for i , msg := range newMessages {
11991224 wg .Add (1 )
12001225 go func (idx int , raw string ) {
12011226 defer wg .Done ()
@@ -1206,7 +1231,8 @@ func (m *model) renderMessages() string {
12061231 rawContent := strings .TrimPrefix (raw , aiStyle .Render ("VibeAuracle: " ))
12071232 // Only render markdown if it's not currently streaming
12081233 if ! strings .HasSuffix (rawContent , subtleStyle .Render ("▌" )) {
1209- content = aiStyle .Render ("VibeAuracle:" ) + "\n " + m .md .Render (rawContent )
1234+ // Use the width-aware persistent cache (Internal O(1) hit)
1235+ content = aiStyle .Render ("VibeAuracle:" ) + "\n " + m .md .Render (rawContent , m .viewport .Width )
12101236 }
12111237 }
12121238
@@ -1216,14 +1242,17 @@ func (m *model) renderMessages() string {
12161242 }
12171243 wg .Wait ()
12181244
1219- var sb strings.Builder
12201245 for i , r := range rendered {
12211246 sb .WriteString (r )
12221247 if i < len (rendered )- 1 {
12231248 sb .WriteString ("\n \n " )
12241249 }
12251250 }
12261251
1252+ m .memoizedView = sb .String ()
1253+ m .lastViewportWidth = m .viewport .Width
1254+ m .lastMessageCount = len (m .messages )
1255+
12271256 if m .brain .Config ().UI .ShowReasoning {
12281257 sb .WriteString ("\n \n " + lipgloss .NewStyle ().Foreground (lipgloss .Color ("#5F5F5F" )).Bold (true ).Render (" ◆ AGENTIC REASONING TRACE" ) + "\n " )
12291258 for _ , log := range m .thinkingLog {
0 commit comments