33Renders buddy face greeting, project scan results, agent recommendations,
44and session summary with tone/language support and ANSI color output.
55"""
6- from typing import Any , Dict , List
6+ import re
7+ from typing import Any , Dict , List , Optional
78
89# ANSI color codes for terminal output
910ANSI_COLORS : Dict [str , str ] = {
6465BUDDY_WRAP_FACE = "\u25d5 \u2304 \u25d5 " # ◕⌄◕
6566BUDDY_WINK_FACE = "\u25d5 \u2040 \u25d5 " # ◕⁀◕
6667
68+ # Default buddy character configuration
69+ DEFAULT_BUDDY_CONFIG : Dict [str , str ] = {
70+ "name" : "Buddy" ,
71+ "face" : BUDDY_FACE ,
72+ "greeting" : "" ,
73+ "farewell" : "" ,
74+ }
75+
76+ # Max length for custom face field (unicode characters)
77+ _MAX_FACE_LENGTH = 10
78+
79+ # Pattern: only printable unicode, no control chars or ASCII letters/digits
80+ _FACE_PATTERN = re .compile (
81+ r"^[^\x00-\x1f\x7f A-Za-z0-9]{1,%d}$" % _MAX_FACE_LENGTH
82+ )
83+
84+
85+ def get_buddy_config (config : Optional [Dict [str , Any ]] = None ) -> Dict [str , str ]:
86+ """Extract and validate buddy customization from config dict.
87+
88+ Args:
89+ config: Parsed codingbuddy.config.json dict. May contain a 'buddy' key.
90+
91+ Returns:
92+ Validated buddy config with defaults for missing/invalid fields.
93+ """
94+ result = dict (DEFAULT_BUDDY_CONFIG )
95+
96+ if not config or not isinstance (config .get ("buddy" ), dict ):
97+ return result
98+
99+ buddy = config ["buddy" ]
100+
101+ # name: string, non-empty, max 30 chars
102+ name = buddy .get ("name" )
103+ if isinstance (name , str ) and 0 < len (name .strip ()) <= 30 :
104+ result ["name" ] = name .strip ()
105+
106+ # face: unicode-only, no ASCII alphanumerics, max _MAX_FACE_LENGTH chars
107+ face = buddy .get ("face" )
108+ if isinstance (face , str ) and _FACE_PATTERN .match (face ):
109+ result ["face" ] = face
110+
111+ # greeting: string, max 100 chars
112+ greeting = buddy .get ("greeting" )
113+ if isinstance (greeting , str ) and 0 < len (greeting .strip ()) <= 100 :
114+ result ["greeting" ] = greeting .strip ()
115+
116+ # farewell: string, max 100 chars
117+ farewell = buddy .get ("farewell" )
118+ if isinstance (farewell , str ) and 0 < len (farewell .strip ()) <= 100 :
119+ result ["farewell" ] = farewell .strip ()
120+
121+ return result
122+
67123# Returning session greetings by tone and language
68124RETURNING_GREETINGS : Dict [str , Dict [str , str ]] = {
69125 "casual" : {
@@ -154,20 +210,28 @@ def _colorize(text: str, color: str) -> str:
154210 return f"{ ansi } { text } { reset } "
155211
156212
157- def render_buddy_face (tone : str , language : str ) -> str :
213+ def render_buddy_face (
214+ tone : str ,
215+ language : str ,
216+ buddy_config : Optional [Dict [str , str ]] = None ,
217+ ) -> str :
158218 """Render the buddy character face with greeting.
159219
160220 Args:
161221 tone: 'casual' or 'formal'
162222 language: Language code (en, ko, ja, zh, es)
223+ buddy_config: Optional buddy customization from get_buddy_config().
163224
164225 Returns:
165226 ASCII art buddy face with greeting message.
166227 """
167- greeting = _get_greeting (tone , language )
228+ bc = buddy_config or DEFAULT_BUDDY_CONFIG
229+ face = bc .get ("face" , BUDDY_FACE )
230+ custom_greeting = bc .get ("greeting" , "" )
231+ greeting = custom_greeting if custom_greeting else _get_greeting (tone , language )
168232 lines = [
169233 "\u256d \u2501 \u2501 \u2501 \u256e " ,
170- f"\u2503 { BUDDY_FACE } \u2503 { greeting } " ,
234+ f"\u2503 { face } \u2503 { greeting } " ,
171235 "\u2570 \u2501 \u2501 \u2501 \u256f " ,
172236 ]
173237 return "\n " .join (lines )
@@ -255,6 +319,7 @@ def render_session_summary(
255319 agents : List [Dict [str , Any ]],
256320 tone : str ,
257321 language : str ,
322+ buddy_config : Optional [Dict [str , str ]] = None ,
258323) -> str :
259324 """Render session summary with buddy character for stop hook.
260325
@@ -267,18 +332,29 @@ def render_session_summary(
267332 eye (str), colorAnsi (str)
268333 tone: 'casual' or 'formal'
269334 language: Language code (en, ko, ja, zh, es)
335+ buddy_config: Optional buddy customization from get_buddy_config().
270336
271337 Returns:
272338 Formatted session summary string.
273339 """
340+ bc = buddy_config or DEFAULT_BUDDY_CONFIG
341+ custom_farewell = bc .get ("farewell" , "" )
342+
274343 greeting = _get_farewell_greeting (tone , language )
275- farewell = _get_farewell_message (tone , language )
344+ farewell = custom_farewell if custom_farewell else _get_farewell_message (tone , language )
345+
346+ # Use custom face for wrap variant: take first char of custom face if set
347+ face = bc .get ("face" , BUDDY_FACE )
348+ if face != BUDDY_FACE :
349+ wrap_face = face # custom face used as-is
350+ else :
351+ wrap_face = BUDDY_WRAP_FACE
276352
277353 parts = []
278354
279355 # Buddy face with farewell greeting
280356 parts .append ("\u256d \u2501 \u2501 \u2501 \u256e " )
281- parts .append (f"\u2503 { BUDDY_WRAP_FACE } \u2503 { greeting } " )
357+ parts .append (f"\u2503 { wrap_face } \u2503 { greeting } " )
282358 parts .append ("\u2570 \u2501 \u2501 \u2501 \u256f " )
283359
284360 # Session stats section (only if data available)
@@ -320,7 +396,7 @@ def render_session_summary(
320396
321397 # Farewell
322398 parts .append ("" )
323- parts .append (f"{ BUDDY_FACE } { farewell } " )
399+ parts .append (f"{ face } { farewell } " )
324400
325401 return "\n " .join (parts )
326402
@@ -358,6 +434,7 @@ def render_returning_session(
358434 pending_context : "Dict[str, Any] | None" ,
359435 tone : str ,
360436 language : str ,
437+ buddy_config : Optional [Dict [str , str ]] = None ,
361438) -> str :
362439 """Render returning session welcome-back display.
363440
@@ -369,17 +446,27 @@ def render_returning_session(
369446 from docs/codingbuddy/context.md parsing. None if no context.
370447 tone: 'casual' or 'formal'
371448 language: Language code (en, ko, ja, zh, es)
449+ buddy_config: Optional buddy customization from get_buddy_config().
372450
373451 Returns:
374452 Formatted returning session greeting string.
375453 """
376- greeting = _get_returning_greeting (tone , language )
454+ bc = buddy_config or DEFAULT_BUDDY_CONFIG
455+ face = bc .get ("face" , BUDDY_FACE )
456+ custom_greeting = bc .get ("greeting" , "" )
457+ greeting = custom_greeting if custom_greeting else _get_returning_greeting (tone , language )
458+
459+ # Wink face variant for continue prompt
460+ if face != BUDDY_FACE :
461+ wink_face = face
462+ else :
463+ wink_face = BUDDY_WINK_FACE
377464
378465 parts = []
379466
380467 # Buddy face with welcome-back greeting
381468 parts .append ("\u256d \u2501 \u2501 \u2501 \u256e " )
382- parts .append (f"\u2503 { BUDDY_FACE } \u2503 { greeting } " )
469+ parts .append (f"\u2503 { face } \u2503 { greeting } " )
383470 parts .append ("\u2570 \u2501 \u2501 \u2501 \u256f " )
384471
385472 # Last session summary
@@ -427,9 +514,9 @@ def render_returning_session(
427514 if pending_context and pending_context .get ("mode" ):
428515 mode = pending_context ["mode" ]
429516 next_mode = "ACT" if mode == "PLAN" else mode
430- parts .append (f"{ BUDDY_WINK_FACE } Type \" { next_mode } \" to continue!" )
517+ parts .append (f"{ wink_face } Type \" { next_mode } \" to continue!" )
431518 else :
432- parts .append (f"{ BUDDY_WINK_FACE } Ready when you are!" )
519+ parts .append (f"{ wink_face } Ready when you are!" )
433520
434521 return "\n " .join (parts )
435522
@@ -441,6 +528,7 @@ def render_session_start(
441528 language : str ,
442529 previous_session : "Dict[str, Any] | None" = None ,
443530 pending_context : "Dict[str, Any] | None" = None ,
531+ buddy_config : Optional [Dict [str , str ]] = None ,
444532) -> str :
445533 """Render complete session-start output.
446534
@@ -455,21 +543,25 @@ def render_session_start(
455543 language: Language code.
456544 previous_session: Optional previous session data for returning users.
457545 pending_context: Optional pending work context from context.md.
546+ buddy_config: Optional buddy customization from get_buddy_config().
458547
459548 Returns:
460549 Complete formatted session-start output.
461550 """
462551 # Returning session path
463552 if previous_session :
464553 parts = [
465- render_returning_session (previous_session , pending_context , tone , language ),
554+ render_returning_session (
555+ previous_session , pending_context , tone , language ,
556+ buddy_config = buddy_config ,
557+ ),
466558 "" ,
467559 render_scan_results (scan ),
468560 ]
469561 else :
470562 # First visit path (default)
471563 parts = [
472- render_buddy_face (tone , language ),
564+ render_buddy_face (tone , language , buddy_config = buddy_config ),
473565 "" ,
474566 render_scan_results (scan ),
475567 ]
0 commit comments