Files
kit/internal/session/manager.go
T
Ed Zynda 0703dd1602 fix: eliminate escape sequence leak from spinner tea.Program instances
Each spinner created a new tea.NewProgram which sent DECRQM queries for
synchronized output mode 2026. When the program exited and restored
cooked terminal mode, the terminal's DECRPM response leaked as visible
^[[?2026;2$y characters. Replace Bubble Tea spinner with a simple
goroutine animation loop writing directly to stderr via lipgloss.
2026-02-25 18:17:25 +03:00

151 lines
3.3 KiB
Go

package session
import (
"fmt"
"sync"
"charm.land/fantasy"
)
// Manager manages session state and auto-saving functionality.
// It provides thread-safe operations for managing a conversation session,
// including automatic persistence to disk after each modification.
type Manager struct {
session *Session
filePath string
mutex sync.RWMutex
}
// NewManager creates a new session manager with a fresh session.
func NewManager(filePath string) *Manager {
return &Manager{
session: NewSession(),
filePath: filePath,
}
}
// NewManagerWithSession creates a new session manager with an existing session.
func NewManagerWithSession(session *Session, filePath string) *Manager {
return &Manager{
session: session,
filePath: filePath,
}
}
// AddMessage adds a fantasy message to the session and auto-saves.
func (m *Manager) AddMessage(msg fantasy.Message) error {
m.mutex.Lock()
defer m.mutex.Unlock()
sessionMsg := ConvertFromFantasyMessage(msg)
m.session.AddMessage(sessionMsg)
if m.filePath != "" {
return m.session.SaveToFile(m.filePath)
}
return nil
}
// AddMessages adds multiple fantasy messages to the session and auto-saves.
func (m *Manager) AddMessages(msgs []fantasy.Message) error {
m.mutex.Lock()
defer m.mutex.Unlock()
for _, msg := range msgs {
sessionMsg := ConvertFromFantasyMessage(msg)
m.session.AddMessage(sessionMsg)
}
if m.filePath != "" {
return m.session.SaveToFile(m.filePath)
}
return nil
}
// ReplaceAllMessages replaces all messages in the session with the provided messages.
func (m *Manager) ReplaceAllMessages(msgs []fantasy.Message) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Clear existing messages
m.session.Messages = []Message{}
// Add all new messages
for _, msg := range msgs {
sessionMsg := ConvertFromFantasyMessage(msg)
m.session.AddMessage(sessionMsg)
}
if m.filePath != "" {
return m.session.SaveToFile(m.filePath)
}
return nil
}
// SetMetadata sets the session metadata.
func (m *Manager) SetMetadata(metadata Metadata) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.session.SetMetadata(metadata)
if m.filePath != "" {
return m.session.SaveToFile(m.filePath)
}
return nil
}
// GetMessages returns all messages as fantasy.Message slice.
func (m *Manager) GetMessages() []fantasy.Message {
m.mutex.RLock()
defer m.mutex.RUnlock()
messages := make([]fantasy.Message, len(m.session.Messages))
for i, msg := range m.session.Messages {
messages[i] = msg.ConvertToFantasyMessage()
}
return messages
}
// GetSession returns a copy of the current session.
func (m *Manager) GetSession() *Session {
m.mutex.RLock()
defer m.mutex.RUnlock()
sessionCopy := *m.session
sessionCopy.Messages = make([]Message, len(m.session.Messages))
copy(sessionCopy.Messages, m.session.Messages)
return &sessionCopy
}
// Save manually saves the session to file.
func (m *Manager) Save() error {
m.mutex.RLock()
defer m.mutex.RUnlock()
if m.filePath == "" {
return fmt.Errorf("no file path specified for session manager")
}
return m.session.SaveToFile(m.filePath)
}
// GetFilePath returns the file path for this session.
func (m *Manager) GetFilePath() string {
return m.filePath
}
// MessageCount returns the number of messages in the session.
func (m *Manager) MessageCount() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.session.Messages)
}