# Prior-Session Sepia Tint — Design

**Date:** 2026-06-10
**Status:** Approved (pending user spec review)
**Track:** `prior_session_sepia_20260610`
**Spec:** [../../../conductor/tracks/prior_session_sepia_20260610/spec.md](../../../conductor/tracks/prior_session_sepia_20260610/spec.md)

## Problem Statement

The current prior-session (Historical Replay) mode uses `theme.get_color("bubble_vendor")` as a
background tint at three call sites in `src/gui_2.py` (lines 1028, 3960, plus the "HISTORICAL
VIEW" banners at 5142 and 5440). This is a **semantic overload** — `bubble_vendor` is the
"Vendor API" role's bubble color, not a dedicated prior-session slot. Theme authors cannot tune
the prior-session feel without changing the Vendor API bubble.

Beyond the missing dedicated slot, the **content** (text, markdown) renders at full
palette saturation. The "obviously looking at the past" cue is carried only by the bg, not by
the prose.

**Pre-existing related bug** (surfaced by the user during brainstorming): code blocks rendered
by `imgui_color_text_edit` are not tonemap-aware. The library has 4 hardcoded palettes (`dark`,
`light`, `mariana`, `retro_blue`) and their colors are never passed through `theme._tone_map()`.
The user has called this "disappointing" because it defeats the primary purpose of the tonemapper:
allowing a light theme to be usable on a bright monitor without searing the user's retinas.
**This bug is NOT fixable in this track** — see "Honest constraint" below.

## Brainstorm Q&A

### Q1. Tint scope (data only vs. data+chrome vs. whole window)

**A1 confirmed sepia. A2 chose "anything that is actually affected by the prior session (is
hosting old information, old state)" → scope (ii) data + chrome inside prior-session views, with
fallback to (iii) whole window if scope (ii) doesn't look "obviously old" in manual review.**

### Q2. Two independent effects (distinct bg + tint) or single transform

**A — Two independent effects, slider only controls the content tint (recommended and chosen).**
New theme slots: `prior_session_bg` (flat warm color), `prior_session_tint` (sepia color),
`prior_session_amount` (per-palette float 0.0-1.0). Slider controls only the amount; bg is
whatever the theme says.

### Q3. (a) Slider location, (b) tint scope, (c) default per-theme, (d) code blocks

**(a) Theme Settings panel** (under the existing Tone Mapping section). Mirrors the
tonemap pattern exactly.

**(b) Scope (ii) data + chrome inside prior-session views**, with fallback to (iii) whole
window only if (ii) doesn't look obviously old.

**(c) 0.3 (subtle)** — the user can slide up if they want more aggressive. The default is
intentionally subtle because the user said "obviously looking at the past" but the
prior-session feature is a niche mode, not a primary view.

**(d) Originally proposed: bundle the code-block tonemap fix into this track.** The user
revealed the pre-existing disappointment and said: *"if you can somehow tint the code blocks
lmk cause right now they are not affected by tonemapping."* The fix was proposed as
"mutate the `Palette` struct's color slots via `editor.get_palette()` and re-apply."

### Q4. Float-only math (HARD CONSTRAINT)

> "Make sure that all math you do is not integer based. I want to have as much accuracy as
> possible for smooth calculations."

Applied to: `apply_prior_tint`, the slider (`slider_float` not `slider_int`), the per-palette
state dict (stores `float` not `int`), the TOML key (`prior_session_amount: float`), the
code-block palette mutation. The transform pipeline never truncates to int.

## Approach: A1 — Per-render explicit transform

Add `apply_prior_tint(rgba, palette) -> rgba` helper in `src/theme_2.py`. Per-palette state
lives in `_prior_session_amount: dict[str, float]` mirroring `_brightness` exactly. Each
prior-session rendering site in `src/gui_2.py` wraps its `theme.get_color()` call with
`apply_prior_tint(...)` (one-line wrap).

### Honest constraint surfaced during self-review

I verified the upstream `imgui_bundle 1.92.5` API before writing the plan:

```
$ python -c "from imgui_bundle import imgui_color_text_edit as ed; help(ed.TextEditor.get_palette)"
get_palette(self) -> imgui_bundle._imgui_bundle.imgui_color_text_edit.TextEditor.PaletteId
$ python -c "from imgui_bundle import imgui_color_text_edit as ed; help(ed.TextEditor.set_palette)"
set_palette(self, a_value: imgui_bundle._imgui_bundle.imgui_color_text_edit.TextEditor.PaletteId) -> None
```

**`PaletteId` is a 4-value enum** (`dark`, `light`, `mariana`, `retro_blue`). There is
no `Palette` struct with mutable per-color slots. The original brainstorm proposed
`apply_color_grades_to_editor_palette(editor, palette)` that would mutate the struct
in-place — but the struct doesn't exist in this API surface.

**This means the code-block tonemap-awareness is NOT fixable in this track** (or any
track that doesn't fork the library). The user's disappointment is real and the
pre-existing behavior persists. The same constraint forced the `multi_themes_20260604`
track to ship a `syntax_palette` enum field rather than custom token colors. The
honest answer is in the spec's §1.1.1.

### Why A1 over A2 (transparent via get_color) and A3 (context manager)

- **A2** would auto-apply sepia inside `theme.get_color()` when `is_viewing_prior_session` is
  True. Rejected: violates the data-oriented "view composes" principle; risks accidentally
  tinting status indicators that must stay saturated.
- **A3** would require a `with prior_session_view():` wrapper at every prior-session site.
  Rejected: same number of wraps as A1 but uglier.

### Float-only math contract

```
result = lerp(desaturate(input), tint_color, amount)
```

- `desaturate(rgba)`: BT.709 luma `0.2126*r + 0.7152*g + 0.0722*b` (all float, all 0.0-1.0).
- `lerp(a, b, t)`: `a + (b - a) * t` per channel, `t` clamped to [0.0, 1.0].
- `apply_prior_tint(rgba, palette)`: identity at amount=0.0; pure tint at amount=1.0; alpha
  passed through unchanged; output is `tuple[float, float, float, float]`.

## File changes (high-level)

| File | Action | Purpose |
|---|---|---|
| `src/theme_2.py` | Modify | Add `_prior_session_amount` dict + 3 accessors; add `_desaturate`, `_lerp_rgba`, `_imvec4_to_rgba`, `_rgba_to_imvec4`, `apply_prior_tint`; add 3 keys to fallback dict; persist to config |
| `src/theme_models.py` | Modify | Add 3 fields to `ThemePalette`; update `from_dict` / `to_dict` / validator |
| `src/gui_2.py` | Modify | 2 `bubble_vendor` → `prior_session_bg` swaps; 4 `apply_prior_tint` wraps at the 2 banners + 2 render functions; 1 new Theme panel section |
| `themes/*.toml` (8 files) | Modify | Add 3 new keys with per-theme defaults |
| `tests/test_prior_session_amount.py` | Create | per-palette dict semantics |
| `tests/test_prior_session_tint.py` | Create | math contract: identity/pure/monotonic/alpha |
| `tests/test_prior_session_toml.py` | Create | round-trip + validation |
| `tests/test_prior_session_render.py` | Create | ImVec4 ↔ tuple round-trip |
| `tests/test_prior_session_persistence.py` | Create | slider → save → restart round-trip |

## Risk assessment

| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Float math accumulates rounding error over many frames | Low | Low | Each `apply_prior_tint` call is independent; no accumulation; output clamped to [0.0, 1.0] |
| The 8 themes don't all have sensible defaults for the new keys | Low | Low | Defaults in the fallback dict cover missing keys |
| Light themes need a different default for `prior_session_bg` (cream vs. dark brown) | Med | Low | Defaults table in the spec sets per-light-theme `prior_session_bg = (235, 220, 190)` |
| Live-gui tests regress because of the new `apply_prior_tint` wraps | Low | Med | Phase 4 batch-verifies the full suite; per `live_gui_test_hardening_20260605` rule, batch is the only verification that matters |
| The scope (ii) "data + chrome inside prior-session views" doesn't look "obviously old" at the 0.3 default | Med | Low | Phase 4 manual smoke escalates to scope (iii) if needed; the wrap is local to 6 sites, easy to expand |
| **Code-block tonemap-awareness disappoints the user again** | High | Low | Explicit §1.1.1 in the spec; the pre-existing bug persists; user was told upfront in the design doc that the API doesn't support per-instance Palette |

## Out of scope (matches spec §9)

- Per-language syntax color customization (upstream limitation)
- Film grain / vignette / scanline post-effect
- Per-theme user overrides via `config.toml` (TOML key is factory default only)
- Modifying the upstream `imgui_color_text_edit` library
- Process-isolation of the pure helper
- Reorganizing the existing tonemap state dicts
