diff --git a/main.go b/main.go index 3994e64..cea3c2f 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,8 @@ type Message struct { // Conversation represents a parsed conversation type Conversation struct { SessionID string `json:"session_id"` - Title string `json:"title"` // custom-title (user-set) or ai-title + Title string `json:"title"` // custom-title (user-set) or ai-title + IsCustomTitle bool `json:"is_custom_title"` // true only when Title came from a user-set custom-title Cwd string `json:"cwd"` FirstTimestamp string `json:"first_timestamp"` LastTimestamp string `json:"last_timestamp"` @@ -360,12 +361,13 @@ func (m model) formatListItem(item listItem, selected bool) string { project = project[:19] + "..." } - // Mark named sessions (custom/ai title) so they're distinguishable from - // rows that fall back to the first user message. ponytail: the glyph is - // ambiguous-width, so a named row may sit one cell narrow on CJK-width - // terminals - cosmetic only, truncate is rune-safe. + // Mark only user-set custom titles. Claude auto-generates an ai-title for + // almost every session, so marking any title would flag nearly every row; + // the ✎ should mean "you named this". ponytail: the glyph is ambiguous-width, + // so a marked row may sit one cell narrow on CJK-width terminals - cosmetic + // only, truncate is rune-safe. topic := getTopic(item.conv) - if item.conv.Title != "" { + if item.conv.IsCustomTitle { topic = "✎ " + topic } topic = truncate(topic, 40) @@ -662,6 +664,7 @@ func parseConversationFile(path string, cutoff time.Time, maxSize int64) (*Conve if raw.Type == "custom-title" { conv.Title = raw.CustomTitle // user-set name wins over ai-title + conv.IsCustomTitle = raw.CustomTitle != "" } else if raw.Type == "ai-title" { if conv.Title == "" { conv.Title = raw.AiTitle diff --git a/main_test.go b/main_test.go index 9fa0207..cf022f7 100644 --- a/main_test.go +++ b/main_test.go @@ -343,11 +343,35 @@ func TestParseConversationFileTitle(t *testing.T) { if conv.Title != "my custom name" { t.Errorf("Title = %q, want %q", conv.Title, "my custom name") } + if !conv.IsCustomTitle { + t.Error("IsCustomTitle should be true when a custom-title is present") + } if got := getTopic(*conv); got != "my custom name" { t.Errorf("getTopic = %q, want title %q", got, "my custom name") } } +func TestParseConversationFileAiTitleNotCustom(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "ai-titled.jsonl") + content := `{"type":"ai-title","aiTitle":"auto name","sessionId":"ai-titled"} +{"type":"user","cwd":"/test","message":{"content":"hello"},"timestamp":"2024-01-15T10:00:00Z"} +` + if err := os.WriteFile(testFile, []byte(content), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + conv, err := parseConversationFile(testFile, time.Time{}, 0) + if err != nil || conv == nil { + t.Fatalf("parseConversationFile failed: %v", err) + } + if conv.Title != "auto name" { + t.Errorf("Title = %q, want %q", conv.Title, "auto name") + } + if conv.IsCustomTitle { + t.Error("IsCustomTitle must be false for an ai-title (auto-generated)") + } +} + func TestGetTopicFallsBackToFirstMessage(t *testing.T) { conv := Conversation{Messages: []Message{{Role: "user", Text: "first msg"}}} if got := getTopic(conv); got != "first msg" { @@ -703,23 +727,35 @@ func TestFormatListItem(t *testing.T) { } func TestFormatListItemNamedSessionMarker(t *testing.T) { - named := listItem{conv: Conversation{ + custom := listItem{conv: Conversation{ SessionID: "s1", Title: "Refactor auth flow", + IsCustomTitle: true, LastTimestamp: "2024-01-15T10:30:00Z", Messages: []Message{{Role: "user", Text: "hi"}}, }} - unnamed := listItem{conv: Conversation{ + // ai-title: has a Title but it's auto-generated - must NOT get the marker. + aiTitled := listItem{conv: Conversation{ SessionID: "s2", + Title: "Some auto generated title", + IsCustomTitle: false, + LastTimestamp: "2024-01-15T10:30:00Z", + Messages: []Message{{Role: "user", Text: "hi"}}, + }} + fallback := listItem{conv: Conversation{ + SessionID: "s3", LastTimestamp: "2024-01-15T10:30:00Z", Messages: []Message{{Role: "user", Text: "just a first message"}}, }} - m := initialModel([]listItem{named, unnamed}, "", nil) + m := initialModel([]listItem{custom, aiTitled, fallback}, "", nil) - if got := m.formatListItem(named, false); !strings.Contains(got, "✎ Refactor auth flow") { - t.Errorf("named session should show the marker, got %q", got) + if got := m.formatListItem(custom, false); !strings.Contains(got, "✎ Refactor auth flow") { + t.Errorf("user-set custom title should show the marker, got %q", got) + } + if got := m.formatListItem(aiTitled, false); strings.Contains(got, "✎") { + t.Errorf("auto ai-title must NOT show the marker, got %q", got) } - if got := m.formatListItem(unnamed, false); strings.Contains(got, "✎") { + if got := m.formatListItem(fallback, false); strings.Contains(got, "✎") { t.Errorf("first-message fallback should not show the marker, got %q", got) } }