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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user