Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.listHeight < 3 {
m.listHeight = 3
}
return m, nil
// Clear so a shrink doesn't leave wider stale rows behind.
return m, tea.ClearScreen

case tea.MouseMsg:
// Determine if mouse is in preview area (below list + separator)
Expand Down Expand Up @@ -260,8 +261,8 @@ func (m model) View() string {

var b strings.Builder

// Table width: 2 + 16 + 2 + 22 + 2 + 34 + 2 + 5 + 2 + 4 + 2 + 6 = 99
tableWidth := 99
// The list spans the full terminal width; TOPIC flexes to fill it.
tableWidth := m.width

// Title line with help right-aligned
title := fmt.Sprintf("ccs · claude code search · %s", version)
Expand Down Expand Up @@ -309,7 +310,8 @@ func (m model) View() string {
previewHeight := m.height - listHeight - 6 // 6 for title + search + blank + header + borders

// Column headers
b.WriteString(fmt.Sprintf(" \033[90m%-16s %-22s %-34s %5s %4s %6s\033[0m\n", "DATE", "PROJECT", "TOPIC", "MSGS", "HITS", "SIZE"))
b.WriteString(fmt.Sprintf(" \033[90m%-*s %-*s %-*s %*s %*s %*s\033[0m\n",
colDate, "DATE", colProject, "PROJECT", m.topicColWidth(), "TOPIC", colMsgs, "MSGS", colHits, "HITS", colSize, "SIZE"))
b.WriteString(strings.Repeat("─", m.width))
b.WriteString("\n")

Expand Down Expand Up @@ -351,16 +353,35 @@ func (m model) View() string {
return b.String()
}

// Fixed list column widths. TOPIC is the flex column - it absorbs the rest of
// the terminal width (see topicColWidth).
const (
colDate = 16
colProject = 22
colMsgs = 5
colHits = 4
colSize = 6
colGap = 2 // spaces between columns
listIndent = 2 // leading " " / "> " on each row
numGaps = 5
)

// topicColWidth flexes the TOPIC column to fill the terminal width.
func (m model) topicColWidth() int {
used := listIndent + colDate + colProject + colMsgs + colHits + colSize + numGaps*colGap
if w := m.width - used; w > 10 {
return w
}
return 10
}

func (m model) formatListItem(item listItem, selected bool) string {
ts := formatTimestamp(item.conv.LastTimestamp)
project := item.conv.Cwd
if idx := strings.LastIndex(project, "/"); idx >= 0 {
project = project[idx+1:]
}
// Truncate project name to fit column
if len(project) > 22 {
project = project[:19] + "..."
}
project = truncate(project, colProject)

// 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;
Expand All @@ -371,7 +392,8 @@ func (m model) formatListItem(item listItem, selected bool) string {
if item.conv.IsCustomTitle {
topic = "✎ " + topic
}
topic = truncate(topic, 34)
tw := m.topicColWidth()
topic = truncate(topic, tw)

// Message count
msgs := len(item.conv.Messages)
Expand All @@ -392,10 +414,11 @@ func (m model) formatListItem(item listItem, selected bool) string {

// Format: date | project | topic | msgs | hits | size (aligned columns)
if selected {
return fmt.Sprintf("%-16s %-22s %-34s %5d %4d %6s", ts, project, topic, msgs, hits, size)
return fmt.Sprintf("%-*s %-*s %-*s %*d %*d %*s",
colDate, ts, colProject, project, tw, topic, colMsgs, msgs, colHits, hits, colSize, size)
}
return fmt.Sprintf("\033[90m%-16s\033[0m \033[1;33m%-22s\033[0m %-34s %5d \033[36m%4d\033[0m \033[35m%6s\033[0m",
ts, project, topic, msgs, hits, size)
return fmt.Sprintf("\033[90m%-*s\033[0m \033[1;33m%-*s\033[0m %-*s %*d \033[36m%*d\033[0m \033[35m%*s\033[0m",
colDate, ts, colProject, project, tw, topic, colMsgs, msgs, colHits, hits, colSize, size)
}

// buildPreviewLines builds the scrollable message lines of a conversation
Expand Down
33 changes: 33 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@ func TestFormatListItemNamedSessionMarker(t *testing.T) {
Messages: []Message{{Role: "user", Text: "just a first message"}},
}}
m := initialModel([]listItem{custom, aiTitled, fallback}, "", nil)
m.width = 120 // give TOPIC room so the title isn't truncated

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)
Expand All @@ -796,6 +797,38 @@ func TestFormatListItemNamedSessionMarker(t *testing.T) {
}
}

func TestTopicColWidthFlexes(t *testing.T) {
fixed := listIndent + colDate + colProject + colMsgs + colHits + colSize + numGaps*colGap
for _, w := range []int{80, 120, 200} {
m := model{width: w}
if got, want := m.topicColWidth(), w-fixed; got != want {
t.Errorf("topicColWidth(width=%d) = %d, want %d", w, got, want)
}
}
// Tiny terminal clamps to a minimum rather than going negative.
if got := (model{width: 20}).topicColWidth(); got != 10 {
t.Errorf("topicColWidth(width=20) = %d, want 10 (min)", got)
}
}

func TestFormatListItemFillsWidth(t *testing.T) {
item := listItem{conv: Conversation{
SessionID: "s",
LastTimestamp: "2024-01-15T10:30:00Z",
Size: 1 << 20,
Messages: []Message{{Role: "user", Text: "hi"}},
}}
for _, w := range []int{80, 120, 200} {
m := initialModel([]listItem{item}, "", nil)
m.width = w
// Selected row has no ANSI codes; its width + the 2-char row prefix
// added by View should fill the terminal exactly.
if got := len(m.formatListItem(item, true)); got != w-listIndent {
t.Errorf("width=%d: row length = %d, want %d", w, got, w-listIndent)
}
}
}

func TestUpdateKeyboardNavigation(t *testing.T) {
items := []listItem{
{conv: Conversation{SessionID: "test-1"}, searchText: "first"},
Expand Down
Loading