feat: crush-inspired TUI improvements, activity bar polish, startup fresh session

TUI improvements:
- semantic icon constants (✓ ◆ ▣ ✕ ⌘ ◌ ◔ ◉)
- pill-style metadata bar with model/provider/session/context
- fuzzy command palette with deterministic subsequence scoring
- input history navigation (ctrl+p / ctrl+n)
- structured tool log cards (▣ tool_name + args preview)
- no-background activity indicator and meta bar for cleaner rendering

Startup behavior:
- default startup creates a fresh session
- explicit --resume <id> restores saved context
- added main_test.go for initializeSession coverage
This commit is contained in:
loveuer
2026-06-24 18:16:53 -07:00
parent f235b8ce90
commit 7b8bad5e62
5 changed files with 549 additions and 79 deletions
+65 -8
View File
@@ -21,7 +21,7 @@ func TestModelRendersChatShell(t *testing.T) {
m = next.(model)
rendered := m.View()
for _, want := range []string{"test-model", "yolo tools", "light", "ctrl+j", "alt+enter"} {
for _, want := range []string{"test-model", "yolo tools", "light", "ctrl+j", "alt+enter", "ctrl+p/ctrl+n history"} {
if !strings.Contains(rendered, want) {
t.Fatalf("rendered view missing %q:\n%s", want, rendered)
}
@@ -123,11 +123,14 @@ func TestRunningStateRendersActivityIndicator(t *testing.T) {
m.resize()
rendered := m.View()
for _, want := range []string{"Working", "thinking...", "esc cancels"} {
for _, want := range []string{iconWorking, "Working", "thinking...", "esc cancels"} {
if !strings.Contains(rendered, want) {
t.Fatalf("running view missing %q:\n%s", want, rendered)
}
}
if backgroundPattern.MatchString(m.activityView()) {
t.Fatalf("activity view should not paint a background:\n%q", m.activityView())
}
}
func TestUserMessageRendersFullWidthBubble(t *testing.T) {
@@ -276,17 +279,71 @@ func TestInputStartsAtOneLineAndGrows(t *testing.T) {
}
}
func TestSlashCommandSuggestions(t *testing.T) {
func TestCommandPaletteFuzzyMatches(t *testing.T) {
m := newModel(context.Background(), nil, Options{ModelName: "test-model", Yolo: true})
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
m = next.(model)
m.input.SetValue("/")
m.input.SetValue("/mdl")
m.afterInputChanged()
rendered := m.View()
for _, want := range []string{"/clear reset context", "/compact compress context", "/exit quit", "/model model/provider/thinking"} {
if !strings.Contains(rendered, want) {
t.Fatalf("rendered suggestions missing %q:\n%s", want, rendered)
plain := stripANSI(m.View())
for _, want := range []string{iconCommand + " Commands", "/model model/provider/thinking"} {
if !strings.Contains(plain, want) {
t.Fatalf("rendered palette missing %q:\n%s", want, plain)
}
}
matches := commandMatches("m")
modelIndex := -1
renameIndex := -1
for i, match := range matches {
switch match.command.Name {
case "/model":
modelIndex = i
case "/rename":
renameIndex = i
}
}
if modelIndex < 0 || renameIndex < 0 || modelIndex > renameIndex {
t.Fatalf("/model should sort before /rename for /m query: %#v", matches)
}
}
func TestInputHistoryNavigation(t *testing.T) {
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
m.rememberInput("first")
m.rememberInput("second")
next, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlP})
m = next.(model)
if got := m.input.Value(); got != "second" {
t.Fatalf("after ctrl+p input = %q", got)
}
next, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlP})
m = next.(model)
if got := m.input.Value(); got != "first" {
t.Fatalf("after second ctrl+p input = %q", got)
}
next, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlN})
m = next.(model)
if got := m.input.Value(); got != "second" {
t.Fatalf("after ctrl+n input = %q", got)
}
next, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlN})
m = next.(model)
if got := m.input.Value(); got != "" {
t.Fatalf("after second ctrl+n input = %q", got)
}
}
func TestToolMessageRendersStructuredCard(t *testing.T) {
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
m.messages = append(m.messages, message{role: roleTool, content: `file_read {"path":"README.md"}`})
plain := stripANSI(m.renderMessages())
for _, want := range []string{iconTool + " file_read", "README.md"} {
if !strings.Contains(plain, want) {
t.Fatalf("tool card missing %q:\n%s", want, plain)
}
}
}