From 8d641e7c30cf982428678945466ccbdbbccc19d0 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 15:38:10 -0700 Subject: [PATCH] fix(tui): render selected chat bubbles clearly --- CHANGELOG.md | 1 + tui/tui.go | 24 ++++++++++++++++++++---- tui/tui_test.go | 4 ++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0157363..5fc04e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,5 +34,6 @@ - Bring shared TUI detail and sort behavior closer to `gitcrawl`: archives open newest-first, group count headers sort like `cnt*`, selected chat messages render before surrounding conversation context, document previews appear before metadata, and detail fields use `key: value` labels. - Keep split-width member tables readable by rendering compact dates instead of truncated ISO timestamps. - Prioritize gitcrawl-style footer muscle-memory controls in compact tmux panes before app-specific extras. +- Render selected chat message bodies with the same transcript marker as their speaker line so detail panes read more like chat. - Force the Bubble Tea program to shut down on terminal signals so interrupted TUIs restore terminal modes and do not leave orphaned tmux panes. - Rename the public package nouns to `config`, `store`, `snapshot`, `mirror`, `state`, `output`, `tui`, and `cache`. diff --git a/tui/tui.go b/tui/tui.go index 9479252..022c872 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -3699,7 +3699,7 @@ func (m model) chatDetailLines(item Item, width int) []string { lines = append(lines, dim(meta)) } if message := chatBodyText(item); message != "" { - lines = append(lines, "", dim(tuiRule(width)), bold("Message")) + lines = append(lines, "", dim(tuiRule(width)), bold("Selected Message")) lines = appendLimitedDetailLines(lines, chatBubbleLines(item, message, true, width), detailBodyLimit(m.compactDetail)) } if title, thread := m.threadSection(item, width); len(thread) > 0 { @@ -3896,6 +3896,20 @@ func indentMarkdownLines(value string, indent, width int) []string { return out } +func prefixedMarkdownLines(value, prefix string, width int) []string { + prefix = strings.TrimRight(prefix, "\t") + raw := markdownLines(value, maxInt(8, width-lipgloss.Width(prefix))) + out := make([]string, 0, len(raw)) + for _, line := range raw { + if line == "" { + out = append(out, strings.TrimRight(prefix, " ")) + continue + } + out = append(out, prefix+line) + } + return out +} + func detailContextLines(item Item, includeTitle bool) []string { var lines []string fields := []string{ @@ -4043,16 +4057,18 @@ func sortChatIndexesByTime(items []Item, indexes []int) { func chatBubbleLines(item Item, text string, selected bool, width int) []string { var lines []string prefix := " " + bodyPrefix := " " if selected { prefix = "> " + bodyPrefix = "> " } - header := joinNonEmpty([]string{itemAuthor(item), shortTimestamp(firstNonEmpty(item.CreatedAt, item.UpdatedAt))}, " ") + header := joinNonEmpty([]string{itemAuthor(item), shortTimestamp(firstNonEmpty(item.CreatedAt, item.UpdatedAt)), rowAge(item)}, " ") if header != "" { lines = append(lines, prefix+header) } - body := indentMarkdownLines(text, lipgloss.Width(prefix)+2, width) + body := prefixedMarkdownLines(text, bodyPrefix, width) if len(body) == 0 { - body = []string{strings.Repeat(" ", lipgloss.Width(prefix)+2) + "(empty)"} + body = []string{bodyPrefix + "(empty)"} } lines = append(lines, body...) return lines diff --git a/tui/tui_test.go b/tui/tui_test.go index b35ff68..7e5ddff 100644 --- a/tui/tui_test.go +++ b/tui/tui_test.go @@ -481,7 +481,7 @@ func TestChatDetailUsesTranscriptShapeBeforeMetadata(t *testing.T) { } lines := m.detailLines(item) joined := strings.Join(lines, "\n") - for _, want := range []string{"general bob", "Thread 1-2/2", "alice", "root message", "> bob", "reply message", "Properties", "url: https://example.com/thread", "IDs", "parent: m1"} { + for _, want := range []string{"general bob", "Selected Message", "Thread 1-2/2", "alice", "root message", "> bob", "> reply message", "Properties", "url: https://example.com/thread", "IDs", "parent: m1"} { if !strings.Contains(joined, want) { t.Fatalf("chat detail missing %q:\n%s", want, joined) } @@ -510,7 +510,7 @@ func TestChatDetailRendersMarkdownTranscriptLikeGitcrawl(t *testing.T) { t.Fatal("missing selected item") } joined := stripANSI(strings.Join(m.detailLinesForWidth(item, 52), "\n")) - for _, want := range []string{"Plan", "- ship columns", "polish preview ", "> agreed", "done", "Properties", "IDs"} { + for _, want := range []string{"Plan", "- ship columns", "polish preview ", "> > agreed", "> done", "Properties", "IDs"} { if !strings.Contains(joined, want) { t.Fatalf("markdown chat detail missing %q:\n%s", want, joined) }