1
0
Fork 0
mirror of https://github.com/imjasonh/cnotes synced 2026-07-07 00:33:04 +00:00

Add conversational flow to cnotes show command

This commit is contained in:
Jason Hall 2025-07-21 14:31:48 -04:00
parent a98459ed4d
commit 588abf835d
Failed to extract signature
2 changed files with 184 additions and 42 deletions

View file

@ -151,15 +151,9 @@ func printConversationMarkdown(note notes.ConversationNote, commit string) {
fmt.Printf("**Claude Version:** %s\n", note.ClaudeVersion)
fmt.Printf("**Tools Used:** %s\n\n", strings.Join(note.ToolsUsed, ", "))
// Commit context
if note.CommitContext != "" {
fmt.Printf("## Commit Context\n\n")
fmt.Printf("```\n%s\n```\n\n", note.CommitContext)
}
// Conversation excerpt
// Conversation transcript
if note.ConversationExcerpt != "" {
fmt.Printf("## Conversation Context\n\n")
fmt.Printf("## Conversation Transcript\n\n")
// Clean up and format the conversation excerpt for better readability
formatted := formatConversationExcerpt(note.ConversationExcerpt)
fmt.Printf("%s\n\n", formatted)
@ -188,8 +182,58 @@ func formatConversationExcerpt(excerpt string) string {
for strings.Contains(formatted, "\n\n\n") {
formatted = strings.ReplaceAll(formatted, "\n\n\n", "\n\n")
}
// Format different message types for better visual distinction
lines := strings.Split(formatted, "\n")
var formattedLines []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
formattedLines = append(formattedLines, "")
continue
}
switch {
case strings.HasPrefix(line, "User:"):
// Bold user prompts
formattedLines = append(formattedLines, "**"+line+"**")
case strings.HasPrefix(line, "Claude:"):
// Keep Claude responses as-is
formattedLines = append(formattedLines, line)
case strings.HasPrefix(line, "Tool ("):
// Format tool uses in code blocks
toolParts := strings.SplitN(line, ": ", 2)
if len(toolParts) == 2 {
formattedLines = append(formattedLines, toolParts[0]+":")
formattedLines = append(formattedLines, "```")
formattedLines = append(formattedLines, toolParts[1])
formattedLines = append(formattedLines, "```")
} else {
formattedLines = append(formattedLines, line)
}
case strings.HasPrefix(line, "Result:"):
// Format results in code blocks
resultParts := strings.SplitN(line, ": ", 2)
if len(resultParts) == 2 {
formattedLines = append(formattedLines, "_"+resultParts[0]+":_")
formattedLines = append(formattedLines, "```")
formattedLines = append(formattedLines, resultParts[1])
formattedLines = append(formattedLines, "```")
} else {
formattedLines = append(formattedLines, line)
}
default:
// Handle multi-line content that might be part of a previous block
formattedLines = append(formattedLines, line)
}
}
return strings.TrimSpace(formatted)
return strings.Join(formattedLines, "\n")
}
// getCommitInfo returns formatted commit information

View file

@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
@ -16,6 +17,15 @@ type ConversationContext struct {
UserPrompts []string `json:"user_prompts"`
ClaudeResponses []string `json:"claude_responses"`
ToolInteractions []ToolInteraction `json:"tool_interactions"`
Events []ConversationEvent `json:"events"` // New: chronological events
}
// ConversationEvent represents any event in the conversation
type ConversationEvent struct {
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"` // "user", "assistant", "tool", "system"
Content string `json:"content"`
ToolName string `json:"tool_name,omitempty"`
}
// ToolInteraction represents a tool use and its result
@ -67,6 +77,7 @@ func (ce *ContextExtractor) ExtractContextSince(transcriptPath string, sessionID
UserPrompts: []string{},
ClaudeResponses: []string{},
ToolInteractions: []ToolInteraction{},
Events: []ConversationEvent{},
}
// Read all transcript files in the directory
@ -92,6 +103,7 @@ func (ce *ContextExtractor) ExtractContextSince(transcriptPath string, sessionID
combinedContext.UserPrompts = append(combinedContext.UserPrompts, context.UserPrompts...)
combinedContext.ClaudeResponses = append(combinedContext.ClaudeResponses, context.ClaudeResponses...)
combinedContext.ToolInteractions = append(combinedContext.ToolInteractions, context.ToolInteractions...)
combinedContext.Events = append(combinedContext.Events, context.Events...)
}
// Apply privacy filters
@ -125,6 +137,7 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, si
UserPrompts: []string{},
ClaudeResponses: []string{},
ToolInteractions: []ToolInteraction{},
Events: []ConversationEvent{},
}
lines := strings.Split(content, "\n")
@ -150,14 +163,15 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, si
}
}
// Extract timestamp
var entryTime time.Time
if timestampStr, ok := entry["timestamp"].(string); ok {
entryTime, _ = time.Parse(time.RFC3339, timestampStr)
}
// Filter by timestamp if provided
if !since.IsZero() {
if timestampStr, ok := entry["timestamp"].(string); ok {
entryTime, err := time.Parse(time.RFC3339, timestampStr)
if err == nil && entryTime.Before(since) {
continue // Skip entries before the cutoff
}
}
if !since.IsZero() && !entryTime.IsZero() && entryTime.Before(since) {
continue // Skip entries before the cutoff
}
// Extract based on type
@ -172,6 +186,12 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, si
// Direct string content
if !strings.Contains(content, "[Request interrupted by user") {
context.UserPrompts = append(context.UserPrompts, content)
// Add to events
context.Events = append(context.Events, ConversationEvent{
Timestamp: entryTime,
Type: "user",
Content: content,
})
}
} else if contentArray, ok := msg["content"].([]interface{}); ok {
// Array of content objects
@ -181,6 +201,12 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, si
// Skip system messages about interruptions
if !strings.Contains(text, "[Request interrupted by user") {
context.UserPrompts = append(context.UserPrompts, text)
// Add to events
context.Events = append(context.Events, ConversationEvent{
Timestamp: entryTime,
Type: "user",
Content: text,
})
}
}
}
@ -189,14 +215,30 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, si
}
case "assistant":
// Extract tool uses from assistant messages
// Extract tool uses and text responses from assistant messages
if msg, ok := entry["message"].(map[string]interface{}); ok {
if content, ok := msg["content"].([]interface{}); ok {
for _, c := range content {
if toolUse, ok := c.(map[string]interface{}); ok {
if toolUse["type"] == "tool_use" {
toolName, _ := toolUse["name"].(string)
if input, ok := toolUse["input"].(map[string]interface{}); ok {
if contentItem, ok := c.(map[string]interface{}); ok {
contentType, _ := contentItem["type"].(string)
switch contentType {
case "text":
// Assistant text response
if text, ok := contentItem["text"].(string); ok && text != "" {
context.ClaudeResponses = append(context.ClaudeResponses, text)
// Add to events
context.Events = append(context.Events, ConversationEvent{
Timestamp: entryTime,
Type: "assistant",
Content: text,
})
}
case "tool_use":
// Tool use
toolName, _ := contentItem["name"].(string)
if input, ok := contentItem["input"].(map[string]interface{}); ok {
interaction := ToolInteraction{
Tool: toolName,
}
@ -228,6 +270,13 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, si
if interaction.Input != "" {
context.ToolInteractions = append(context.ToolInteractions, interaction)
// Add to events
context.Events = append(context.Events, ConversationEvent{
Timestamp: entryTime,
Type: "tool",
Content: interaction.Input,
ToolName: toolName,
})
}
}
}
@ -235,6 +284,29 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, si
}
}
}
case "tool_result":
// Extract tool results
if result, ok := entry["result"].(map[string]interface{}); ok {
var resultContent string
toolName, _ := entry["tool_name"].(string)
if stdout, ok := result["stdout"].(string); ok && stdout != "" {
resultContent = stdout
} else if output, ok := result["output"].(string); ok && output != "" {
resultContent = output
}
if resultContent != "" {
// Add to events
context.Events = append(context.Events, ConversationEvent{
Timestamp: entryTime,
Type: "tool_result",
Content: resultContent,
ToolName: toolName,
})
}
}
}
}
@ -273,32 +345,58 @@ func (ce *ContextExtractor) sanitizeText(text string) string {
// CreateExcerpt creates a concise excerpt from conversation context
func (ce *ContextExtractor) CreateExcerpt(context *ConversationContext) string {
// Sort events by timestamp
sort.Slice(context.Events, func(i, j int) bool {
return context.Events[i].Timestamp.Before(context.Events[j].Timestamp)
})
var parts []string
// Include all user prompts (they're already filtered by time)
if len(context.UserPrompts) > 0 {
parts = append(parts, "User prompts since last commit:")
for _, prompt := range context.UserPrompts {
if len(prompt) > 300 {
prompt = prompt[:297] + "..."
for _, event := range context.Events {
var line string
switch event.Type {
case "user":
// Format user prompts
content := event.Content
if len(content) > 200 {
content = content[:197] + "..."
}
parts = append(parts, fmt.Sprintf("- %s", prompt))
line = fmt.Sprintf("User: %s", content)
case "assistant":
// Format assistant responses
content := event.Content
if len(content) > 200 {
content = content[:197] + "..."
}
line = fmt.Sprintf("Claude: %s", content)
case "tool":
// Format tool uses
content := event.Content
if len(content) > 150 {
content = content[:147] + "..."
}
line = fmt.Sprintf("Tool (%s): %s", event.ToolName, content)
case "tool_result":
// Format tool results - show abbreviated output
content := event.Content
lines := strings.Split(content, "\n")
if len(lines) > 3 {
content = strings.Join(lines[:3], "\n") + "\n[...]"
} else if len(content) > 150 {
content = content[:147] + "..."
}
line = fmt.Sprintf("Result: %s", content)
}
if line != "" {
parts = append(parts, line)
}
}
// Include all tool interactions (they're already filtered by time)
if len(context.ToolInteractions) > 0 {
parts = append(parts, "\nTool interactions since last commit:")
for _, interaction := range context.ToolInteractions {
input := interaction.Input
if len(input) > 100 {
input = input[:97] + "..."
}
parts = append(parts, fmt.Sprintf("- %s: %s", interaction.Tool, input))
}
}
excerpt := strings.Join(parts, "\n")
excerpt := strings.Join(parts, "\n\n")
// Truncate if too long
if len(excerpt) > ce.maxExcerptLength {