diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-02 10:20:08 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-02 10:20:08 -0400 |
| commit | 82b195278b551839314edf1bfab37d5f54c432cb (patch) | |
| tree | 181e6aae830e63048c919525c583dc156e058c05 | |
| parent | 4b53b52507faef662a8f670b4b80cbe24290de95 (diff) | |
| download | dotemacs-82b195278b551839314edf1bfab37d5f54c432cb.tar.gz dotemacs-82b195278b551839314edf1bfab37d5f54c432cb.zip | |
feat(ai-term): render Claude Code session colors in dupre hues
Claude Code's /color picks a session accent from eight names, each emitted as a fixed xterm-256 index (probed against v2.1.198 by cycling /color in a scratch tmux session and reading the SGR codes). Agent terminals now pin all eight indices plus the bypass banner to dupre faces, so any /color choice renders in the theme's palette instead of stock xterm hues. dupre has no orange or pink, so those borrow red+1 and magenta+1. If a Claude Code update moves an index, the stock hue comes back (the alist docstring carries the re-probe note).
| -rw-r--r-- | modules/ai-term-backend-eat.el | 24 | ||||
| -rw-r--r-- | modules/ai-term.el | 67 | ||||
| -rw-r--r-- | tests/test-ai-term--accent.el | 69 |
3 files changed, 113 insertions, 47 deletions
diff --git a/modules/ai-term-backend-eat.el b/modules/ai-term-backend-eat.el index 82218ca7..21385e37 100644 --- a/modules/ai-term-backend-eat.el +++ b/modules/ai-term-backend-eat.el @@ -30,7 +30,7 @@ (defvar eat-buffer-name) (defvar eat-semi-char-mode-map) (defvar eat-terminal) -(defvar cj/ai-term-accent-color-indices) +(defvar cj/ai-term-palette-faces) (defun cj/--ai-term-send-string (buffer string) "Send STRING to BUFFER's terminal process (the agent's shell). @@ -40,20 +40,20 @@ Sends to the pty directly so the launch command reaches the shell EAT runs." (process-send-string proc string)))) (defun cj/--ai-term-apply-accent (buffer) - "Point BUFFER's terminal accent palette entries at `cj/ai-term-accent'. -Repaints each index in `cj/ai-term-accent-color-indices' in this -terminal's own 256-color palette (eat keeps one per terminal), so the -agent's accent -- Claude Code's rose banner, borders, spinner -- renders -in the accent face's color while every other eat terminal keeps the true -palette. A no-op when BUFFER has no live eat terminal. Takes effect on -the terminal's next redraw; text already on screen keeps its old color -until the program repaints it (Claude Code's TUI repaints continuously)." + "Point BUFFER's terminal palette entries at their dupre faces. +Repaints each (INDEX . FACE) in `cj/ai-term-palette-faces' in this +terminal's own 256-color palette (eat keeps one per terminal), so Claude +Code's accents -- the bypass banner and every /color session color -- +render in dupre hues while other eat terminals keep the true palette. +A no-op when BUFFER has no live eat terminal. Takes effect on the +terminal's next redraw; text already on screen keeps its old color until +the program repaints it (Claude Code's TUI repaints continuously)." (with-current-buffer buffer (when (bound-and-true-p eat-terminal) - (dolist (index cj/ai-term-accent-color-indices) + (dolist (entry cj/ai-term-palette-faces) (eat-term-set-parameter eat-terminal - (intern (format "color-%d-face" index)) - 'cj/ai-term-accent))))) + (intern (format "color-%d-face" (car entry))) + (cdr entry)))))) (defun cj/--ai-term-show-or-create (dir name) "Show or create the AI-term buffer for project DIR with buffer NAME. diff --git a/modules/ai-term.el b/modules/ai-term.el index 0774758e..bd955292 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -117,22 +117,61 @@ fallback when `cj/--ai-term-last-size' is nil." (defface cj/ai-term-accent '((t :foreground "#67809c")) - "Accent color for agent terminals, defaulting to dupre blue. -Claude Code draws its accent (the bypass-permissions banner, borders, -spinner) with xterm-256 palette colors; agent terminals point those -palette entries at this face (see `cj/ai-term-accent-color-indices'), -so the accent renders in this face's foreground instead of the stock -rose red. Per-project colors later land as per-buffer overrides of -the same palette entries." + "Default accent for agent terminals: dupre blue. +Carries the elements Claude Code colors regardless of the session color +-- the bypass-permissions banner (xterm palette index 211, stock rose). +The /color session accents get their own faces below." :group 'ai-term) -(defvar cj/ai-term-accent-color-indices '(211) - "The xterm-256 palette indices agent terminals repaint with the accent. -211 (#ff87af, a rose pink) is Claude Code's accent as rendered through -the 256-color palette -- confirmed empirically on the bypass-permissions -banner. Add indices here if other accent elements surface in a -different palette slot. Applied per terminal by -`cj/--ai-term-apply-accent'; other eat terminals keep the true palette.") +;; One face per Claude Code /color session color, each pinned to its dupre +;; counterpart. Claude Code emits a fixed xterm-256 index per color name +;; (probed empirically against v2.1.198); `cj/ai-term-palette-faces' maps +;; those indices to these faces, and agent terminals render them instead of +;; the stock xterm hues. dupre has no orange or pink, so orange borrows +;; bright red (peach) and pink borrows bright magenta. +(defface cj/ai-term-color-red '((t :foreground "#d47c59")) + "Agent terminal rendering of Claude Code's red session color (dupre red)." + :group 'ai-term) +(defface cj/ai-term-color-blue '((t :foreground "#67809c")) + "Agent terminal rendering of Claude Code's blue session color (dupre blue)." + :group 'ai-term) +(defface cj/ai-term-color-green '((t :foreground "#a4ac64")) + "Agent terminal rendering of Claude Code's green session color (dupre green)." + :group 'ai-term) +(defface cj/ai-term-color-yellow '((t :foreground "#d7af5f")) + "Agent terminal rendering of Claude Code's yellow session color (dupre yellow)." + :group 'ai-term) +(defface cj/ai-term-color-purple '((t :foreground "#b294bb")) + "Agent terminal rendering of Claude Code's purple session color (dupre magenta)." + :group 'ai-term) +(defface cj/ai-term-color-orange '((t :foreground "#edb08f")) + "Agent terminal rendering of Claude Code's orange session color (dupre red+1)." + :group 'ai-term) +(defface cj/ai-term-color-pink '((t :foreground "#c397d8")) + "Agent terminal rendering of Claude Code's pink session color (dupre magenta+1)." + :group 'ai-term) +(defface cj/ai-term-color-cyan '((t :foreground "#8a9496")) + "Agent terminal rendering of Claude Code's cyan session color (dupre steel)." + :group 'ai-term) + +(defvar cj/ai-term-palette-faces + '((211 . cj/ai-term-accent) ; bypass banner (fixed, not a /color) + (167 . cj/ai-term-color-red) + (110 . cj/ai-term-color-blue) + (35 . cj/ai-term-color-green) + (178 . cj/ai-term-color-yellow) + (140 . cj/ai-term-color-purple) + (174 . cj/ai-term-color-orange) + (175 . cj/ai-term-color-pink) + (37 . cj/ai-term-color-cyan)) + "Alist of (XTERM-256-INDEX . FACE) repainted in agent terminals. +The indices are what Claude Code v2.1.198 emits: 211 for the fixed +bypass-permissions banner, the rest one per /color session color +\(probed by cycling /color in a scratch session and reading the SGR +codes). Applied per terminal by `cj/--ai-term-apply-accent', so other +eat terminals keep the true xterm palette. If a Claude Code update +moves an accent to a new index, the visible symptom is the stock xterm +hue coming back -- re-probe and update the index here.") ;; Agent buffers ("agent [<project>]") are buried, not killed, by the ;; kill-all sweep (F1 / `cj/dashboard-only'). Register the family pattern so diff --git a/tests/test-ai-term--accent.el b/tests/test-ai-term--accent.el index 7ffd71a1..2f381bab 100644 --- a/tests/test-ai-term--accent.el +++ b/tests/test-ai-term--accent.el @@ -1,10 +1,11 @@ -;;; test-ai-term--accent.el --- Tests for the agent-terminal accent color -*- lexical-binding: t; -*- +;;; test-ai-term--accent.el --- Tests for the agent-terminal accent colors -*- lexical-binding: t; -*- ;;; Commentary: -;; Tests the per-terminal accent recolor: the accent face's dupre-blue default, -;; the palette-index list, the apply helper that points a terminal's 256-color -;; palette entries at the accent face, and the show-or-create wiring. eat and -;; its terminal API are stubbed -- no process spawning, no eat load in batch. +;; Tests the per-terminal palette recolor: the dupre faces for Claude Code's +;; session colors, the palette-index-to-face alist, the apply helper that +;; points a terminal's 256-color palette entries at those faces, and the +;; show-or-create wiring. eat and its terminal API are stubbed -- no process +;; spawning, no eat load in batch. ;;; Code: @@ -17,7 +18,7 @@ (declare-function cj/--ai-term-apply-accent "ai-term-backend-eat" (buffer)) (declare-function cj/--ai-term-show-or-create "ai-term-backend-eat" (dir name)) -(defvar cj/ai-term-accent-color-indices) +(defvar cj/ai-term-palette-faces) (defvar cj/--ai-term-mru) (defvar eat-buffer-name) (defvar eat-terminal) @@ -28,44 +29,70 @@ (unless (fboundp 'eat-term-set-parameter) (defun eat-term-set-parameter (_terminal _parameter _value) nil)) -;;; ------------------------------ accent face --------------------------------- +;;; ------------------------------ accent faces -------------------------------- (ert-deftest test-ai-term-accent-face-defaults-to-dupre-blue () - "Normal: the accent face exists and defaults to dupre blue (#67809c)." + "Normal: the default accent face exists and is dupre blue (#67809c)." (should (facep 'cj/ai-term-accent)) (should (string-equal-ignore-case (face-attribute 'cj/ai-term-accent :foreground nil t) "#67809c"))) -(ert-deftest test-ai-term-accent-indices-default () - "Normal: the remapped palette indices default to Claude Code's accent (211)." - (should (equal cj/ai-term-accent-color-indices '(211)))) +(ert-deftest test-ai-term-session-color-faces-carry-dupre-hues () + "Normal: each of Claude Code's session colors has a face with its dupre hue." + (dolist (pair '((cj/ai-term-color-red . "#d47c59") + (cj/ai-term-color-blue . "#67809c") + (cj/ai-term-color-green . "#a4ac64") + (cj/ai-term-color-yellow . "#d7af5f") + (cj/ai-term-color-purple . "#b294bb") + (cj/ai-term-color-orange . "#edb08f") + (cj/ai-term-color-pink . "#c397d8") + (cj/ai-term-color-cyan . "#8a9496"))) + (should (facep (car pair))) + (should (string-equal-ignore-case + (face-attribute (car pair) :foreground nil t) + (cdr pair))))) + +(ert-deftest test-ai-term-palette-faces-cover-banner-and-session-colors () + "Normal: the alist pins the bypass banner (211) and all 8 session-color indices." + (should (eq (alist-get 211 cj/ai-term-palette-faces) 'cj/ai-term-accent)) + (dolist (pair '((167 . cj/ai-term-color-red) + (110 . cj/ai-term-color-blue) + (35 . cj/ai-term-color-green) + (178 . cj/ai-term-color-yellow) + (140 . cj/ai-term-color-purple) + (174 . cj/ai-term-color-orange) + (175 . cj/ai-term-color-pink) + (37 . cj/ai-term-color-cyan))) + (should (eq (alist-get (car pair) cj/ai-term-palette-faces) (cdr pair))))) ;;; --------------------------- cj/--ai-term-apply-accent ---------------------- (ert-deftest test-ai-term-apply-accent-sets-palette-entries () - "Normal: every configured index is pointed at the accent face via the eat API." - (let ((set-params nil)) + "Normal: every alist entry is pointed at its face via the eat API." + (let ((set-params nil) + (cj/ai-term-palette-faces '((211 . cj/ai-term-accent) + (110 . cj/ai-term-color-blue)))) (cl-letf (((symbol-function 'eat-term-set-parameter) (lambda (_terminal parameter value) (push (cons parameter value) set-params)))) (with-temp-buffer (setq-local eat-terminal 'dummy-terminal) (cj/--ai-term-apply-accent (current-buffer)))) - (should (equal set-params '((color-211-face . cj/ai-term-accent)))))) + (should (equal (nreverse set-params) + '((color-211-face . cj/ai-term-accent) + (color-110-face . cj/ai-term-color-blue)))))) -(ert-deftest test-ai-term-apply-accent-multiple-indices () - "Boundary: several indices each get their own palette-entry call." - (let ((set-params nil) - (cj/ai-term-accent-color-indices '(211 174))) +(ert-deftest test-ai-term-apply-accent-full-alist-count () + "Boundary: the default alist yields one palette call per entry (9 total)." + (let ((set-params nil)) (cl-letf (((symbol-function 'eat-term-set-parameter) (lambda (_terminal parameter value) (push (cons parameter value) set-params)))) (with-temp-buffer (setq-local eat-terminal 'dummy-terminal) (cj/--ai-term-apply-accent (current-buffer)))) - (should (equal (sort (mapcar #'car set-params) #'string<) - '(color-174-face color-211-face))))) + (should (= (length set-params) (length cj/ai-term-palette-faces))))) (ert-deftest test-ai-term-apply-accent-no-terminal-is-noop () "Error: a buffer without a live eat terminal is left alone, no API call, no error." @@ -80,7 +107,7 @@ ;;; ------------------------- show-or-create wiring ---------------------------- (ert-deftest test-ai-term-show-or-create-applies-accent-on-create () - "Normal: creating a fresh agent terminal applies the accent to its buffer." + "Normal: creating a fresh agent terminal applies the palette to its buffer." (let ((name "agent [accent-wire-test]") (cj/--ai-term-mru nil) (applied nil)) |
