Skip to content

Commit 20524aa

Browse files
committed
feat(chat): mark auto-compaction threshold on context bar
Draw a thin marker on the mode-line context bar at the point where auto-compaction triggers (pixel segment in graphical frames, a glyph in terminals), store autoCompactPercentage from the usage content, and note it in the bar tooltip.
1 parent 3b817ac commit 20524aa

2 files changed

Lines changed: 107 additions & 18 deletions

File tree

eca-chat.el

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,13 @@ behavior)."
607607
"Face for the free-space segment of the context-usage bar."
608608
:group 'eca)
609609

610+
(defface eca-chat-context-compaction-marker-face
611+
'((((background dark)) (:foreground "#d7dae0"))
612+
(((background light)) (:foreground "#383a42")))
613+
"Face for the auto-compaction threshold marker on the context bar.
614+
Its foreground is also used as the marker color in graphical frames."
615+
:group 'eca)
616+
610617
(defface eca-chat-elapsed-time-face
611618
'((t :height 0.9 :inherit font-lock-comment-face))
612619
"Face for the elapsed time indicator in mode-line of the chat."
@@ -712,6 +719,9 @@ whether the first user prompt should erase the banner first.")
712719
(defvar-local eca-chat--session-tokens nil)
713720
(defvar-local eca-chat--session-limit-context nil)
714721
(defvar-local eca-chat--session-limit-output nil)
722+
(defvar-local eca-chat--session-auto-compact-percentage nil
723+
"Context-window percentage at which the server auto-compacts.
724+
Nil when auto-compaction is disabled or unknown.")
715725
(defvar-local eca-chat--context-breakdown nil
716726
"Latest context-window usage breakdown plist for the current chat.
717727
Keys: :categories (vector of :name/:tokens plists), :usedTokens,
@@ -1091,6 +1101,14 @@ Replace the job status emoji with NEW-EMOJI."
10911101
:type 'integer
10921102
:group 'eca)
10931103

1104+
(defcustom eca-chat-context-bar-show-compaction-marker t
1105+
"Whether to mark the auto-compaction threshold on the context bar."
1106+
:type 'boolean
1107+
:group 'eca)
1108+
1109+
(defconst eca-chat--context-bar-marker-px 2
1110+
"Width in pixels of the auto-compaction marker in graphical frames.")
1111+
10941112
(defconst eca-chat--context-category-faces
10951113
'(("System prompt" . eca-chat-context-system-prompt-face)
10961114
("Rules" . eca-chat-context-rules-face)
@@ -1138,11 +1156,12 @@ Prefers the server color, falling back to the client face foreground."
11381156
(face-foreground 'eca-chat-context-free-face nil t)
11391157
"#5c6370")))
11401158

1141-
(defun eca-chat--context-bar-help (breakdown used free limit)
1159+
(defun eca-chat--context-bar-help (breakdown used free limit &optional compact-pct)
11421160
"Build the help-echo legend for the context bar from BREAKDOWN.
11431161
USED, FREE and LIMIT are the aggregate token counts. Each line is
11441162
prefixed with a colored swatch matching the bar so the colors map to
1145-
their category."
1163+
their category. COMPACT-PCT, when set, notes the auto-compaction
1164+
threshold percentage."
11461165
(let* ((nf #'eca-chat--number->friendly-number)
11471166
;; Prefer the server-provided emoji swatch (consistent with the
11481167
;; /context text); fall back to a colored block for older servers.
@@ -1168,6 +1187,11 @@ their category."
11681187
(funcall swatch (plist-get breakdown :freeEmoji)
11691188
(eca-chat--context-free-face-spec breakdown))
11701189
(funcall nf free)))
1190+
(when (and compact-pct (numberp compact-pct) (> compact-pct 0))
1191+
(format "\nAuto-compaction at %s%%"
1192+
(if (= compact-pct (round compact-pct))
1193+
(number-to-string (round compact-pct))
1194+
(format "%.1f" compact-pct))))
11711195
"\n\nClick to run /context")))
11721196

11731197
(defvar eca-chat--context-bar-mode-line-map
@@ -1220,11 +1244,12 @@ number of cells available when there are more categories than cells)."
12201244
when (> n 0) do (setq last cat))
12211245
(eca-chat--context-category-face-spec (or last (car categories)))))
12221246

1223-
(defun eca-chat--context-bar-chars (categories breakdown width frac)
1247+
(defun eca-chat--context-bar-chars (categories breakdown width frac &optional marker-frac)
12241248
"Build the block-character context bar string for terminal frames.
12251249
CATEGORIES/BREAKDOWN carry the usage data, WIDTH is the cell count and
12261250
FRAC the used fraction (0..1). The used/free edge uses a fractional
1227-
block for sub-cell precision."
1251+
block for sub-cell precision. MARKER-FRAC, when non-nil, draws the
1252+
auto-compaction threshold marker on the matching cell."
12281253
(let* ((colored-exact (* width frac))
12291254
(colored-cells (min width (floor colored-exact)))
12301255
(edge-eighths (if (< colored-cells width)
@@ -1244,34 +1269,69 @@ block for sub-cell precision."
12441269
(when (> free-cells 0)
12451270
(setq bar (concat bar (propertize (make-string free-cells ?█)
12461271
'face (eca-chat--context-free-face-spec breakdown))))))
1272+
(when (and marker-frac (> (length bar) 0))
1273+
(let ((idx (min (1- (length bar))
1274+
(max 0 (floor (* width (min 1.0 (max 0.0 marker-frac))))))))
1275+
(setq bar (concat (substring bar 0 idx)
1276+
(propertize "" 'face 'eca-chat-context-compaction-marker-face)
1277+
(substring bar (1+ idx))))))
12471278
bar))
12481279

1249-
(defun eca-chat--context-bar-pixels (categories breakdown width frac)
1280+
(defun eca-chat--context-bar-pixels-insert-marker (segs marker-x)
1281+
"Overlay the auto-compaction marker on SEGS near MARKER-X pixels.
1282+
SEGS is a list of (PX . COLOR); return a new list with the same total
1283+
width, the marker overwriting `eca-chat--context-bar-marker-px' pixels
1284+
clamped to stay fully visible."
1285+
(let* ((total (apply #'+ 0 (mapcar #'car segs)))
1286+
(mw (min eca-chat--context-bar-marker-px total))
1287+
(m-start (max 0 (min marker-x (- total mw))))
1288+
(m-end (+ m-start mw))
1289+
(mcolor (or (face-foreground 'eca-chat-context-compaction-marker-face nil t)
1290+
"#888888"))
1291+
(x 0)
1292+
(out nil))
1293+
(dolist (s segs)
1294+
(let* ((px (car s)) (color (cdr s))
1295+
(start x) (end (+ x px))
1296+
(le (max start (min end m-start)))
1297+
(me (max start (min end m-end))))
1298+
(when (> (- le start) 0) (push (cons (- le start) color) out))
1299+
(when (> (- me le) 0) (push (cons (- me le) mcolor) out))
1300+
(when (> (- end me) 0) (push (cons (- end me) color) out))
1301+
(setq x end)))
1302+
(nreverse out)))
1303+
1304+
(defun eca-chat--context-bar-pixels (categories breakdown width frac &optional marker-frac)
12501305
"Build the pixel-width thin-segment context bar for graphical frames.
12511306
CATEGORIES/BREAKDOWN carry the usage data; each category is a
12521307
`:background'-colored space whose pixel width is proportional to its
12531308
share, giving sub-character granularity so small percentages still
12541309
show as a thin sliver. WIDTH is the footprint in characters and FRAC
1255-
the used fraction (0..1)."
1310+
the used fraction (0..1). MARKER-FRAC, when non-nil, overlays the
1311+
auto-compaction threshold marker at that fraction of the bar."
12561312
(let* ((char-px (max 1 (frame-char-width)))
12571313
(total-px (* width char-px))
12581314
(used-px (min total-px (round (* total-px frac))))
12591315
(tokens (mapcar (lambda (c) (max 0 (or (plist-get c :tokens) 0))) categories))
12601316
(alloc (eca-chat--context-bar-allocate tokens used-px))
1261-
(seg (lambda (px color)
1262-
(propertize " "
1263-
'display (list 'space :width (list px))
1264-
'face (list :background color))))
1265-
(bar "")
1317+
(segs nil)
12661318
(drawn 0))
12671319
(cl-loop for cat in categories for px in alloc
12681320
when (> px 0)
1269-
do (setq bar (concat bar (funcall seg px (eca-chat--context-category-color cat)))
1270-
drawn (+ drawn px)))
1321+
do (push (cons px (eca-chat--context-category-color cat)) segs)
1322+
(setq drawn (+ drawn px)))
12711323
(let ((free-px (- total-px drawn)))
12721324
(when (> free-px 0)
1273-
(setq bar (concat bar (funcall seg free-px (eca-chat--context-free-color breakdown))))))
1274-
bar))
1325+
(push (cons free-px (eca-chat--context-free-color breakdown)) segs)))
1326+
(setq segs (nreverse segs))
1327+
(when (and marker-frac segs)
1328+
(setq segs (eca-chat--context-bar-pixels-insert-marker
1329+
segs (round (* total-px (min 1.0 (max 0.0 marker-frac)))))))
1330+
(mapconcat (lambda (s)
1331+
(propertize " "
1332+
'display (list 'space :width (list (car s)))
1333+
'face (list :background (cdr s))))
1334+
segs "")))
12751335

12761336
(defun eca-chat--context-bar ()
12771337
"Return the propertized context-usage bar string, or nil.
@@ -1287,10 +1347,15 @@ per-category tokens are in the tooltip."
12871347
(limit (plist-get breakdown :contextLimit))
12881348
(width (max 1 eca-chat-context-bar-width))
12891349
(frac (if (and limit (> limit 0)) (min 1.0 (/ (float used) limit)) 1.0))
1290-
(help (eca-chat--context-bar-help breakdown used free limit))
1350+
(compact-pct (when (and eca-chat-context-bar-show-compaction-marker
1351+
(numberp eca-chat--session-auto-compact-percentage)
1352+
(> eca-chat--session-auto-compact-percentage 0))
1353+
eca-chat--session-auto-compact-percentage))
1354+
(marker-frac (when compact-pct (min 1.0 (/ (float compact-pct) 100.0))))
1355+
(help (eca-chat--context-bar-help breakdown used free limit compact-pct))
12911356
(bar (if (display-graphic-p)
1292-
(eca-chat--context-bar-pixels categories breakdown width frac)
1293-
(eca-chat--context-bar-chars categories breakdown width frac))))
1357+
(eca-chat--context-bar-pixels categories breakdown width frac marker-frac)
1358+
(eca-chat--context-bar-chars categories breakdown width frac marker-frac))))
12941359
(propertize bar
12951360
'help-echo help
12961361
'local-map eca-chat--context-bar-mode-line-map))))
@@ -3981,6 +4046,7 @@ Must be called with `eca-chat--with-current-buffer' or equivalent."
39814046
(setq-local eca-chat--session-limit-output (plist-get (plist-get content :limit) :output))
39824047
(setq-local eca-chat--message-cost (plist-get content :messageCost))
39834048
(setq-local eca-chat--session-cost (plist-get content :sessionCost))
4049+
(setq-local eca-chat--session-auto-compact-percentage (plist-get content :autoCompactPercentage))
39844050
(setq-local eca-chat--context-breakdown (plist-get content :contextBreakdown)))
39854051
(force-mode-line-update)))
39864052
(_ nil))

test/eca-chat-test.el

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,4 +1088,27 @@ does not treat the first line as metadata. Returns FN's value."
10881088
16 0.5)))
10891089
(expect (> (eca-chat--string-pixel-width bar) (length bar)) :to-be t))))
10901090

1091+
(describe "eca-chat context-bar compaction marker"
1092+
(it "keeps the pixel-bar total width when the marker is overlaid"
1093+
(let* ((cats (list (list :name "System prompt" :tokens 100 :color "#ff0000")))
1094+
(bd (list :freeColor "#222222"))
1095+
(plain (eca-chat--context-bar-pixels cats bd 16 0.5))
1096+
(marked (eca-chat--context-bar-pixels cats bd 16 0.5 0.75)))
1097+
(expect (eca-chat--string-pixel-width marked)
1098+
:to-equal (eca-chat--string-pixel-width plain))))
1099+
1100+
(it "draws the marker glyph on the terminal bar without changing length"
1101+
(let* ((cats (list (list :name "System prompt" :tokens 100 :color "#ff0000")))
1102+
(bd (list :freeColor "#222222"))
1103+
(plain (eca-chat--context-bar-chars cats bd 8 1.0))
1104+
(marked (eca-chat--context-bar-chars cats bd 8 1.0 0.5)))
1105+
(expect (length marked) :to-equal (length plain))
1106+
(expect (string-match-p "" marked) :to-be-truthy)))
1107+
1108+
(it "notes the auto-compaction threshold in the tooltip"
1109+
(let* ((breakdown (list :categories (vector (list :name "System prompt" :tokens 5300 :emoji "🟦"))
1110+
:usedTokens 5300 :freeTokens 194700 :contextLimit 200000))
1111+
(help (eca-chat--context-bar-help breakdown 5300 194700 200000 75)))
1112+
(expect help :to-match "Auto-compaction at 75%"))))
1113+
10911114
;;; eca-chat-test.el ends here

0 commit comments

Comments
 (0)