Skip to content

Commit 34ce243

Browse files
committed
fix(plugin): batch achievement notifications to prevent text wall (#1042)
Add render_batch_celebration() that shows the top achievement in detail and summarises the rest as "+N more achievements unlocked!" when multiple achievements are unlocked simultaneously.
1 parent 761a406 commit 34ce243

3 files changed

Lines changed: 93 additions & 5 deletions

File tree

packages/claude-code-plugin/hooks/lib/achievement_tracker.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,27 @@
7373
"en": {
7474
"unlocked": "Achievement Unlocked!",
7575
"congrats": "Congratulations!",
76+
"more_unlocked": "+{n} more achievements unlocked!",
7677
},
7778
"ko": {
7879
"unlocked": "업적 달성!",
7980
"congrats": "축하합니다!",
81+
"more_unlocked": "+{n}개의 업적을 추가 달성!",
8082
},
8183
"ja": {
8284
"unlocked": "実績解除!",
8385
"congrats": "おめでとうございます!",
86+
"more_unlocked": "+{n}件の実績を解除!",
8487
},
8588
"zh": {
8689
"unlocked": "成就解锁!",
8790
"congrats": "恭喜!",
91+
"more_unlocked": "+{n}个成就已解锁!",
8892
},
8993
"es": {
9094
"unlocked": "Logro desbloqueado!",
9195
"congrats": "Felicitaciones!",
96+
"more_unlocked": "+{n} logros más desbloqueados!",
9297
},
9398
}
9499

@@ -383,6 +388,37 @@ def render_achievement_celebration(
383388
return "\n".join(lines)
384389

385390

391+
def render_batch_celebration(
392+
newly_unlocked: List[Dict[str, Any]],
393+
language: str = "en",
394+
) -> str:
395+
"""Render a batch celebration for one or more newly unlocked achievements.
396+
397+
When a single achievement is unlocked, delegates to render_achievement_celebration.
398+
When multiple are unlocked, shows the first in detail and summarises the rest.
399+
400+
Args:
401+
newly_unlocked: List of achievement definition dicts just unlocked.
402+
language: Language code.
403+
404+
Returns:
405+
Formatted celebration string, or empty string if list is empty.
406+
"""
407+
if not newly_unlocked:
408+
return ""
409+
410+
if len(newly_unlocked) == 1:
411+
return render_achievement_celebration(newly_unlocked[0], language)
412+
413+
# Multiple: show first achievement in detail, summarise the rest
414+
top = newly_unlocked[0]
415+
detail = render_achievement_celebration(top, language)
416+
msgs = CELEBRATION_MESSAGES.get(language, CELEBRATION_MESSAGES["en"])
417+
remaining = len(newly_unlocked) - 1
418+
summary_line = f" {msgs['more_unlocked'].format(n=remaining)}"
419+
return f"{detail}\n{summary_line}"
420+
421+
386422
def render_achievement_badges(
387423
unlocked: List[Dict[str, Any]],
388424
language: str = "en",

packages/claude-code-plugin/hooks/stop.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def handle_stop(data: dict):
131131
try:
132132
from achievement_tracker import (
133133
AchievementTracker,
134-
render_achievement_celebration,
134+
render_batch_celebration,
135135
render_achievement_badges,
136136
)
137137

@@ -145,10 +145,10 @@ def handle_stop(data: dict):
145145

146146
newly_unlocked = tracker.check_achievements()
147147
if newly_unlocked:
148-
for achievement in newly_unlocked:
149-
celebration = render_achievement_celebration(
150-
achievement, language
151-
)
148+
celebration = render_batch_celebration(
149+
newly_unlocked, language
150+
)
151+
if celebration:
152152
print(celebration, file=sys.stderr)
153153

154154
# Show badge summary if any unlocked

packages/claude-code-plugin/hooks/tests/test_achievement_tracker.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
get_achievement_definitions,
2121
render_achievement_badges,
2222
render_achievement_celebration,
23+
render_batch_celebration,
2324
)
2425

2526

@@ -350,3 +351,54 @@ def test_render_unknown_id(self):
350351
unlocked = [{"id": "unknown_xyz", "name": "Mystery"}]
351352
result = render_achievement_badges(unlocked, "en")
352353
assert "Mystery" in result
354+
355+
356+
class TestRenderBatchCelebration:
357+
"""Test batch celebration rendering (#1042)."""
358+
359+
def test_empty_list(self):
360+
result = render_batch_celebration([], "en")
361+
assert result == ""
362+
363+
def test_single_achievement(self):
364+
defn = get_achievement_by_id("tdd_champion")
365+
result = render_batch_celebration([defn], "en")
366+
expected = render_achievement_celebration(defn, "en")
367+
assert result == expected
368+
369+
def test_three_achievements(self):
370+
ids = ["tdd_champion", "agent_master", "streak"]
371+
achievements = []
372+
for aid in ids:
373+
defn = get_achievement_by_id(aid)
374+
if defn:
375+
achievements.append(defn)
376+
# Pad with fallback dicts if not enough real definitions
377+
while len(achievements) < 3:
378+
achievements.append({"name": f"Test{len(achievements)}", "icon": "\U0001f3c6"})
379+
380+
result = render_batch_celebration(achievements, "en")
381+
# First achievement should be detailed
382+
assert achievements[0]["name"] in result
383+
assert "Achievement Unlocked!" in result
384+
# Summary line for remaining
385+
assert "+2 more achievements unlocked!" in result
386+
387+
def test_three_achievements_korean(self):
388+
achievements = [
389+
{"name": "First", "icon": "\U0001f3c6", "face": "\u2605\u203f\u2605"},
390+
{"name": "Second", "icon": "\U0001f3c6"},
391+
{"name": "Third", "icon": "\U0001f3c6"},
392+
]
393+
result = render_batch_celebration(achievements, "ko")
394+
assert "업적 달성!" in result
395+
assert "+2개의 업적을 추가 달성!" in result
396+
397+
def test_two_achievements(self):
398+
achievements = [
399+
{"name": "Alpha", "icon": "\U0001f3c6", "face": "\u2605\u203f\u2605"},
400+
{"name": "Beta", "icon": "\U0001f3c6"},
401+
]
402+
result = render_batch_celebration(achievements, "en")
403+
assert "Alpha" in result
404+
assert "+1 more achievements unlocked!" in result

0 commit comments

Comments
 (0)