adamdottv 8 месяцев назад
Родитель
Сommit
167aea6aaf

+ 88 - 0
packages/tui/internal/components/chat/cache.go

@@ -0,0 +1,88 @@
+package chat
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"sync"
+
+	"github.com/sst/opencode/pkg/client"
+)
+
+// MessageCache caches rendered messages to avoid re-rendering
+type MessageCache struct {
+	mu    sync.RWMutex
+	cache map[string]string
+}
+
+// NewMessageCache creates a new message cache
+func NewMessageCache() *MessageCache {
+	return &MessageCache{
+		cache: make(map[string]string),
+	}
+}
+
+// generateKey creates a unique key for a message based on its content and rendering parameters
+func (c *MessageCache) generateKey(msg client.MessageInfo, width int, showToolMessages bool, appInfo client.AppInfo) string {
+	// Create a hash of the message content and rendering parameters
+	h := sha256.New()
+
+	// Include message ID and role
+	h.Write(fmt.Appendf(nil, "%s:%s", msg.Id, msg.Role))
+
+	// Include timestamp
+	h.Write(fmt.Appendf(nil, ":%f", msg.Metadata.Time.Created))
+
+	// Include width and showToolMessages flag
+	h.Write(fmt.Appendf(nil, ":%d:%t", width, showToolMessages))
+
+	// Include app path for relative path calculations
+	h.Write([]byte(appInfo.Path.Root))
+
+	// Include message parts
+	for _, part := range msg.Parts {
+		h.Write(fmt.Appendf(nil, ":%v", part))
+	}
+
+	// Include tool metadata if present
+	for toolID, metadata := range msg.Metadata.Tool {
+		h.Write(fmt.Appendf(nil, ":%s:%v", toolID, metadata))
+	}
+
+	return hex.EncodeToString(h.Sum(nil))
+}
+
+// Get retrieves a cached rendered message
+func (c *MessageCache) Get(msg client.MessageInfo, width int, showToolMessages bool, appInfo client.AppInfo) (string, bool) {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	key := c.generateKey(msg, width, showToolMessages, appInfo)
+	content, exists := c.cache[key]
+	return content, exists
+}
+
+// Set stores a rendered message in the cache
+func (c *MessageCache) Set(msg client.MessageInfo, width int, showToolMessages bool, appInfo client.AppInfo, content string) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	key := c.generateKey(msg, width, showToolMessages, appInfo)
+	c.cache[key] = content
+}
+
+// Clear removes all entries from the cache
+func (c *MessageCache) Clear() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.cache = make(map[string]string)
+}
+
+// Size returns the number of cached entries
+func (c *MessageCache) Size() int {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+
+	return len(c.cache)
+}

+ 24 - 2
packages/tui/internal/components/chat/messages.go

@@ -25,6 +25,7 @@ type messagesCmp struct {
 	rendering        bool
 	rendering        bool
 	attachments      viewport.Model
 	attachments      viewport.Model
 	showToolMessages bool
 	showToolMessages bool
+	cache            *MessageCache
 }
 }
 type renderFinishedMsg struct{}
 type renderFinishedMsg struct{}
 type ToggleToolMessagesMsg struct{}
 type ToggleToolMessagesMsg struct{}
@@ -63,6 +64,7 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
 	switch msg := msg.(type) {
 	case dialog.ThemeChangedMsg:
 	case dialog.ThemeChangedMsg:
+		m.cache.Clear()
 		m.renderView()
 		m.renderView()
 		return m, nil
 		return m, nil
 	case ToggleToolMessagesMsg:
 	case ToggleToolMessagesMsg:
@@ -70,9 +72,13 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		m.renderView()
 		m.renderView()
 		return m, nil
 		return m, nil
 	case state.SessionSelectedMsg:
 	case state.SessionSelectedMsg:
+		// Clear cache when switching sessions
+		m.cache.Clear()
 		cmd := m.Reload()
 		cmd := m.Reload()
 		return m, cmd
 		return m, cmd
 	case state.SessionClearedMsg:
 	case state.SessionClearedMsg:
+		// Clear cache when session is cleared
+		m.cache.Clear()
 		cmd := m.Reload()
 		cmd := m.Reload()
 		return m, cmd
 		return m, cmd
 	case tea.KeyMsg:
 	case tea.KeyMsg:
@@ -103,12 +109,23 @@ func (m *messagesCmp) renderView() {
 
 
 	messages := make([]string, 0)
 	messages := make([]string, 0)
 	for _, msg := range m.app.Messages {
 	for _, msg := range m.app.Messages {
+		var content string
+		var cached bool
+
 		switch msg.Role {
 		switch msg.Role {
 		case client.User:
 		case client.User:
-			content := renderUserMessage(m.app.Info.User, msg, m.width)
+			content, cached = m.cache.Get(msg, m.width, m.showToolMessages, *m.app.Info)
+			if !cached {
+				content = renderUserMessage(m.app.Info.User, msg, m.width)
+				m.cache.Set(msg, m.width, m.showToolMessages, *m.app.Info, content)
+			}
 			messages = append(messages, content+"\n")
 			messages = append(messages, content+"\n")
 		case client.Assistant:
 		case client.Assistant:
-			content := renderAssistantMessage(msg, m.width, m.showToolMessages, *m.app.Info)
+			content, cached = m.cache.Get(msg, m.width, m.showToolMessages, *m.app.Info)
+			if !cached {
+				content = renderAssistantMessage(msg, m.width, m.showToolMessages, *m.app.Info)
+				m.cache.Set(msg, m.width, m.showToolMessages, *m.app.Info, content)
+			}
 			messages = append(messages, content+"\n")
 			messages = append(messages, content+"\n")
 		}
 		}
 	}
 	}
@@ -287,6 +304,10 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
 	if m.width == width && m.height == height {
 	if m.width == width && m.height == height {
 		return nil
 		return nil
 	}
 	}
+	// Clear cache on resize since width affects rendering
+	if m.width != width {
+		m.cache.Clear()
+	}
 	m.width = width
 	m.width = width
 	m.height = height
 	m.height = height
 	m.viewport.Width = width
 	m.viewport.Width = width
@@ -338,5 +359,6 @@ func NewMessagesCmp(app *app.App) tea.Model {
 		spinner:          s,
 		spinner:          s,
 		attachments:      attachments,
 		attachments:      attachments,
 		showToolMessages: true,
 		showToolMessages: true,
+		cache:            NewMessageCache(),
 	}
 	}
 }
 }