diff --git a/helpers.go b/helpers.go index 5b42533..826ef48 100644 --- a/helpers.go +++ b/helpers.go @@ -48,6 +48,14 @@ func UnmarshalChatsTEXT(data []byte) (*Chats, error) { } func (all *Chats) AddFile(filename string) error { + // Nil checks for safety. + if all == nil { + return fmt.Errorf("cannot call AddFile on a nil *Chats object") + } + if all.Chats == nil { + all.Chats = make([]*Chat, 0) + } + data, err := os.ReadFile(filename) if err != nil { log.Fatalf("Error reading file %s: %v", filename, err) @@ -60,35 +68,89 @@ func (all *Chats) AddFile(filename string) error { return err } - for _, chat := range logData.GetChats() { - // make a copy - newc := proto.Clone(chat).(*Chat) - - // Handle content: prefer content_file, fallback to content. - var content string - if contentFile := chat.GetContentFile(); contentFile != "" { - // Construct the full path relative to the log file's directory. - logDir := filepath.Dir(filename) - contentPath := filepath.Join(logDir, contentFile) - - contentBytes, err := os.ReadFile(contentPath) - if err != nil { - content = fmt.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err) - } else { - content = string(contentBytes) + // Iterate through the top-level messages from the source file. + for _, chatGroup := range logData.GetChats() { + if len(chatGroup.GetEntries()) > 0 { + // NEW FORMAT: This is a group, process its entries. + for _, entry := range chatGroup.GetEntries() { + // Convert the ChatEntry into a new Chat object for the flat list. + newChat := convertEntryToChat(entry, filename) + if newChat != nil { + newChat.VerifyUuid() + all.AppendByUuid(newChat) + } } } else { - // Fallback for older log formats. - content = chat.GetContent() + // OLD FORMAT: This is a single, flat Chat message. + // We still process it to handle its external content file correctly. + newChat := convertChatToChat(chatGroup, filename) + if newChat != nil { + newChat.VerifyUuid() + all.AppendByUuid(newChat) + } } - newc.Content = content - newc.VerifyUuid() - all.AppendNew(newc) } - return nil } +// convertChatToChat handles an old-style Chat message. It creates a clean +// copy and resolves its external content file. +func convertChatToChat(chat *Chat, filename string) *Chat { + if chat == nil { + return nil + } + + // Manually create a ChatEntry from the Chat fields to reuse the logic. + entry := &ChatEntry{ + From: chat.GetFrom(), + Ctime: chat.GetCtime(), + Content: chat.GetContent(), + Table: chat.GetTable(), + ToolCalls: chat.GetToolCalls(), + ContentFile: chat.GetContentFile(), + Uuid: chat.GetUuid(), + Snippets: chat.GetSnippets(), + } + return convertEntryToChat(entry, filename) +} + +// convertEntryToChat creates a new Chat object from a ChatEntry's data +// and resolves its external content file. +func convertEntryToChat(entry *ChatEntry, filename string) *Chat { + if entry == nil { + return nil + } + + // Create a new Chat object and copy the fields. + newChat := &Chat{ + From: entry.GetFrom(), + Ctime: entry.GetCtime(), + Table: entry.GetTable(), + ToolCalls: entry.GetToolCalls(), + Snippets: entry.GetSnippets(), + Uuid: entry.GetUuid(), + } + + // Handle content: prefer content_file, fallback to content. + var content string + if contentFile := entry.GetContentFile(); contentFile != "" { + logDir := filepath.Dir(filename) + contentPath := filepath.Join(logDir, contentFile) + contentBytes, err := os.ReadFile(contentPath) + if err != nil { + content = fmt.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err) + } else { + content = string(contentBytes) + } + } else { + content = entry.GetContent() + } + newChat.Content = content + + return newChat +} + + func (chats *Chats) VerifyUuids() bool { var changed bool