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
6 changes: 3 additions & 3 deletions internal/tui/drawer.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ func (m *Model) runRows(w int) []string {
if n := len(r.PendingApprovals); n > 0 {
meta += " · ⚠ " + plural(n, "approval", "approvals")
}
label := runStatusGlyph(r) + " " + orDash(r.Model)
label := runStatusGlyph(r) + " " + orDash(collapse(r.Model))
if r.Result != "" {
label += " — " + truncate(collapse(r.Result), 30)
} else if r.Error != "" {
Expand Down Expand Up @@ -541,9 +541,9 @@ func (m *Model) eventRows(w int) []string {
if !ev.Timestamp.IsZero() {
ts = ev.Timestamp.Format("15:04:05")
}
label := ts + " " + ev.Type
label := ts + " " + collapse(ev.Type)
if ev.Tool != "" {
label += " · " + ev.Tool
label += " · " + collapse(ev.Tool)
}
if ev.Iteration > 0 {
label += fmt.Sprintf(" · iter %d", ev.Iteration)
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/palette.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (m *Model) handlePalSessions(msg palSessionsMsg) tea.Cmd {
task = "(untitled)"
}
m.pal.all = append(m.pal.all, palEntry{
title: "resume · " + task,
title: "resume · " + collapse(task),
hint: "^R",
kind: "session",
run: func(m *Model) tea.Cmd { return m.resumeSession(sess.ID) },
Expand Down
46 changes: 46 additions & 0 deletions internal/tui/w1_hardening_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,52 @@ func TestModelCatalogSanitizesPickerAndPalette(t *testing.T) {
}
}

// Palette session titles copy sess.Task raw; the sessions list already
// collapse()s the same field.
func TestPaletteSessionTitleSanitizesTask(t *testing.T) {
const payload = "\x1b[2J"
m := newTestModel()
m.pal.open = true
m.handlePalSessions(palSessionsMsg{items: []client.Session{{
ID: "s1", Task: "x" + payload + "evil",
}}})
for _, e := range m.pal.all {
if e.kind == "session" && strings.Contains(e.title, payload) {
t.Fatalf("palette session title leaked CSI: %q", e.title)
}
}
if pop := m.palPopup(); strings.Contains(pop, payload) {
t.Fatalf("palette popup leaked CSI: %q", pop)
}
}

// Runs tab collapses Result/Error but paints r.Model raw.
func TestRunRowsSanitizeModel(t *testing.T) {
const payload = "\x1b[2J"
m := newTestModel()
m.runs = []client.Run{{
ID: "r1", Status: "completed",
Model: "x" + payload + "evil",
Result: "ok",
}}
if strings.Contains(strings.Join(m.runRows(80), "\n"), payload) {
t.Fatal("runs row leaked model CSI")
}
}

// Events tab concatenates ev.Type / ev.Tool with no collapse.
func TestEventRowsSanitizeTypeAndTool(t *testing.T) {
const payload = "\x1b[2J"
m := newTestModel()
m.feed = []client.RuntimeEvent{{
Type: "tool_call" + payload,
Tool: "shell" + payload,
}}
if strings.Contains(strings.Join(m.eventRows(80), "\n"), payload) {
t.Fatal("events row leaked tool CSI")
}
}

// The conversation is the click/scroll hit-test source of truth: a line that
// physically wraps desyncs every index below it. Every emitted line must fit
// the viewport.
Expand Down