# ADR-0021: Cosmetics — Unified Indicator + Splash with Persona System

**Status:** Accepted (supersedes ADR-0008, ADR-0011)
**Date:** 2026-05-16
**Extension:** `extensions/cosmetics/`

## Problem

Three separate extensions (`cn3-indicator/`, `kirby-indicator/`, `splash-screen/`) with massive duplication:

1. Both indicator extensions hooked the same lifecycle events independently — kirby always ran, overriding cn3.
2. The `/juergen` toggle flipped a flag in cn3 but kirby-indicator ignored it completely.
3. The splash screen had Kirby art/spiral code but no connection to the toggle.
4. ~300 lines of identical event handling logic duplicated across both indicators.

## Decision

Single `cosmetics/` extension with an extensible **persona system**:

| Component             | Responsibility                                                              |
| --------------------- | --------------------------------------------------------------------------- |
| `types.ts`            | `Persona` interface, `ActivityStatus`, `StreamPhase`, unified `TOOL_PHASES` |
| `state.ts`            | Persona registry, active state, `cyclePersona()`, `onSwitch()` listener     |
| `indicator/engine.ts` | **One** event loop that delegates rendering to the active persona           |
| `indicator/kirby.ts`  | Kirby indicator persona (ASCII art + baked-in messages)                     |
| `splash/launcher.ts`  | Picks splash component based on active persona                              |
| `splash/kirby.ts`     | Kirby splash (braille art over spiral background)                           |

**Default persona:** `kirby` (Juergen mode).

### Key Design Points

- **Single event loop:** One place handles agent_start, turn_start, message_update, tool_call, tool_result, turn_end, agent_end. No racing, no override battles.
- **Persona interface:** `{ id, name, frameIntervalMs, getFrames(status), getMessage(status) }` for indicators; `{ createComponent(tui, theme, done) }` for splash.
- **Hot-switching:** `onSwitch()` callback forces re-push of current status frames immediately when persona changes mid-session.
- **Extensible:** Add new personas by implementing the interface and calling `registerPersona()`.

## Alternatives

| Approach                               | Why Rejected                                                  |
| -------------------------------------- | ------------------------------------------------------------- |
| Keep separate extensions + shared flag | Still races; flag must be global mutable state across modules |
| Single extension with if/else branches | Doesn't scale; new personas require touching the engine       |
| Settings-based config                  | Can't switch mid-session; no `/juergen` UX                    |

## Trade-offs

- Slightly more files than a single monolithic extension, but each is small (<100 lines) and focused.
- Persona registration is imperative (in `index.ts`), not declarative — acceptable for 2-3 personas.
- Frame caches are per-persona, so memory usage doubles. Negligible in practice (~10KB strings).

## Evidence

- Before: kirby-indicator always overrode cn3 (confirmed by tracing event hooks).
- After: only one set of event listeners exists; persona switching tested via unit tests.
- 21 unit tests cover phase mapping, frame generation, and persona API contracts.
