diff --git a/internal/tui/permission_prompt_test.go b/internal/tui/permission_prompt_test.go index 34d3df547..34101a26f 100644 --- a/internal/tui/permission_prompt_test.go +++ b/internal/tui/permission_prompt_test.go @@ -2,6 +2,7 @@ package tui import ( "context" + "fmt" "strings" "testing" @@ -376,3 +377,58 @@ func TestShiftDownComposerGuard(t *testing.T) { t.Fatalf("Shift+Down with non-empty composer scrolled offset to %d, want 3 (unchanged)", got) } } + +// The highlighted permission option must use the selected-row tint, not the +// brand chip. zeroTheme.badge is the accent-filled chip for short labels +// (" 0 ", " ASK ", " SPEC REVIEW "); using it for a whole row painted a +// full-brightness accent slab across a card whose palette is deliberately amber +// (warning), and skipped the card tint every other line composes onto. selBg is +// the tint tuned for a highlighted row against a panel, and onSel is what every +// other selectable list in the TUI uses. +func TestFocusedPermissionSelectedRowUsesSelectionTintNotBrandChip(t *testing.T) { + request := agent.PermissionRequest{ToolName: "exec_command", SideEffect: "shell"} + card, _ := renderFocusedPermissionPrompt(request, 0, 70) + + var selected string + for _, line := range strings.Split(card, "\n") { + if strings.Contains(ansiPattern.ReplaceAllString(line, ""), "▸ ") { + selected = line + break + } + } + if selected == "" { + t.Fatal("no highlighted option row rendered") + } + + accentBg := backgroundCode(darkPalette.accent) + selBg := backgroundCode(darkPalette.selBg) + if strings.Contains(selected, accentBg) { + t.Errorf("selected row is filled with the brand accent %s (zeroTheme.badge); want the selection tint:\n%q", darkPalette.accent, selected) + } + if !strings.Contains(selected, selBg) { + t.Errorf("selected row should carry the selection tint %s:\n%q", darkPalette.selBg, selected) + } + + // The PERMISSION chip keeps its amber fill — the card must still read as a + // warning surface, so this fix must not flatten it. + if !strings.Contains(card, backgroundCode(darkPalette.amber)) { + t.Errorf("PERMISSION badge lost its amber fill:\n%q", card) + } + + // The card BODY carries no warm permBg wash any more: it matches the other + // prompt cards (ask_user, spec) whose bodies are transparent. Warning identity + // comes from the amber badge + border, not a full-body tint that clashes on + // cool themes. + if strings.Contains(card, backgroundCode(darkPalette.permBg)) { + t.Errorf("permission card body still tinted with permBg %s; want a transparent body:\n%q", darkPalette.permBg, card) + } +} + +// backgroundCode renders the SGR truecolor background sequence for a #rrggbb +// palette entry, so assertions compare against the palette rather than +// hardcoded numbers that drift when a theme is retuned. +func backgroundCode(hex string) string { + var r, g, b int + fmt.Sscanf(hex, "#%02x%02x%02x", &r, &g, &b) + return fmt.Sprintf("48;2;%d;%d;%d", r, g, b) +} diff --git a/internal/tui/rendering.go b/internal/tui/rendering.go index bdf651734..28e09d7ad 100644 --- a/internal/tui/rendering.go +++ b/internal/tui/rendering.go @@ -1112,7 +1112,14 @@ func renderFocusedPermissionPrompt(request agent.PermissionRequest, cursor int, if name == "" { name = "tool" } - fill := zeroTheme.onPerm + // The card body carries no background fill, matching every other prompt card + // (ask_user, spec review, plan) — see the lipgloss.NewStyle() fills below. The + // permission card used to tint its whole body with permBg, an amber-family + // wash that reads as a warm slab on cool themes (e.g. a brown-yellow box over + // dracula's purples) and made it the one outlier. The amber PERMISSION badge + // and the amber-mixed border still carry the "this is a permission gate" + // signal; the body no longer clashes with the surrounding theme. + fill := func(style lipgloss.Style) lipgloss.Style { return style } top := zeroTheme.permBadge.Render(" PERMISSION ") @@ -1147,8 +1154,16 @@ func renderFocusedPermissionPrompt(request agent.PermissionRequest, cursor int, hotkey := fill(zeroTheme.faint).Render(" [" + option.hotkey + "]") optionLabel := permissionOptionLabel(option, request) if index == cursor { + // onSel, not badge. zeroTheme.badge is the brand chip (" 0 ", " ASK ", + // " SPEC REVIEW ") — a full-brightness accent fill meant for short + // labels. Using it for a selected ROW painted a bright accent slab + // across the permission card, fighting the card's amber warning palette + // and ignoring the card tint every other line composes onto. selBg is + // the tint tuned for exactly this job ("separates from the panel while + // ink label contrast stays ~9.4:1"), and onSel is what every other + // selectable list in the TUI uses for its highlighted row. marker := fill(zeroTheme.accent).Render("▸ ") - label := zeroTheme.badge.Render(" " + optionLabel + " ") + label := zeroTheme.onSel(zeroTheme.ink).Bold(true).Render(" " + optionLabel + " ") lines = append(lines, marker+label+hotkey) } else { label := fill(zeroTheme.ink).Render(optionLabel) @@ -1163,7 +1178,7 @@ func renderFocusedPermissionPrompt(request agent.PermissionRequest, cursor int, } lines = append(lines, fill(zeroTheme.faint).Render(footer)) - return styledBlockFill(width, lines, zeroTheme.permBorder, zeroTheme.permBg), offsets + return styledBlockFill(width, lines, zeroTheme.permBorder, lipgloss.NewStyle()), offsets } func permissionScopeLine(request agent.PermissionRequest, scope string) string {