// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later // Package messaging owns bounded, session-only conversation history. Application // adapters supply copied messages; the reader owns input, output and security. package messaging import "strings" const HistorySize = 12 const maxProviders = 32 // Message contains no native objects. Provider is the unique accessibility bus // name, so restarting an application cannot expose its previous instance's chat. type Message struct { Provider, Conversation, Window, RoomName, Text string Typing bool } type providerHistory struct { provider string messages []Message } // History is owned by the reader event loop. Focus changes retain it; window // destruction retires that window's messages, and screen lock clears it all. type History struct{ providers []providerHistory } func (history *History) Add(message Message) bool { message.Text = strings.Trim(message.Text, "\n") if message.Typing || strings.TrimSpace(message.Text) == "" || message.Provider == "" || message.Conversation == "" || message.Window == "" { return false } characters := []rune(message.Text) message.Text = string(characters[:min(len(characters), 4096)]) index := -1 for i := range history.providers { if history.providers[i].provider == message.Provider { index = i break } } if index < 0 { if len(history.providers) == maxProviders { history.providers = history.providers[1:] } history.providers = append(history.providers, providerHistory{provider: message.Provider}) index = len(history.providers) - 1 } provider := &history.providers[index] if len(provider.messages) == HistorySize { provider.messages = provider.messages[1:] } provider.messages = append(provider.messages, message) return true } // Previous uses zero for the newest message, matching Cthulhu+F1. func (history *History) Previous(provider string, index int) (Message, bool) { if index < 0 || index >= HistorySize { return Message{}, false } for _, entry := range history.providers { if entry.provider == provider && index < len(entry.messages) { return entry.messages[len(entry.messages)-1-index], true } } return Message{}, false } func (history *History) Retire(provider, path string) { for i := len(history.providers) - 1; i >= 0; i-- { entry := &history.providers[i] if entry.provider != provider { continue } messages := entry.messages[:0] for _, message := range entry.messages { if message.Window != path && message.Conversation != path { messages = append(messages, message) } } clear(entry.messages[len(messages):]) entry.messages = messages if len(messages) == 0 { history.providers = append(history.providers[:i], history.providers[i+1:]...) } } } func (history *History) Reset() { history.providers = nil }