Private
Public Access
0
0

conductor(plan): prior_session_sepia_20260610 spec + design + metadata

New track for prior-session sepia tint:
- 3 new theme slots (prior_session_bg, prior_session_tint, prior_session_amount)
- per-palette state dict mirroring _brightness/_contrast/_gamma
- apply_prior_tint helper (float-only math per user requirement)
- 6 prior-session render sites wrapped (2 bubble_vendor swaps + 4 tint wraps)
- Theme Settings panel slider with persistence

Code-block tonemap fix is OUT OF SCOPE (upstream imgui_bundle 1.92.5
API only exposes 4-value PaletteId enum, no per-instance struct).
See spec §1.1.1 and design doc 'Honest constraint' section.
This commit is contained in:
2026-06-10 23:00:29 -04:00
parent 498c3478fa
commit e1287a4cf4
3 changed files with 823 additions and 0 deletions
@@ -0,0 +1,70 @@
{
"track_id": "prior_session_sepia_20260610",
"name": "Prior-Session Sepia Tint",
"initialized": "2026-06-10",
"owner": "tier2-tech-lead",
"priority": "C",
"status": "planning",
"type": "feature",
"scope": {
"new_files": [
"tests/test_prior_session_amount.py",
"tests/test_prior_session_tint.py",
"tests/test_prior_session_toml.py",
"tests/test_prior_session_render.py",
"tests/test_prior_session_persistence.py"
],
"modified_files": [
"src/theme_2.py",
"src/theme_models.py",
"src/gui_2.py",
"themes/10x_dark.toml",
"themes/binks.toml",
"themes/gruvbox_dark.toml",
"themes/monokai.toml",
"themes/moss.toml",
"themes/nord_dark.toml",
"themes/solarized_dark.toml",
"themes/solarized_light.toml"
]
},
"blocked_by": [],
"blocks": [],
"estimated_phases": 4,
"spec": "spec.md",
"plan": "plan.md (to be authored by writing-plans skill)",
"design_doc": "../../docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md",
"parent_track": null,
"sibling_tracks": [
"multi_themes_20260604",
"prior_session_test_harden_20260605"
],
"architectural_invariant": "All math in the sepia transform pipeline is float (no integer truncation). The transform is composed in the view layer (one-line wraps at 6 prior-session call sites in src/gui_2.py) rather than auto-applied by theme.get_color() — the view composes, per the data-oriented principle in product-guidelines.md.",
"math_constraint": "FLOAT-ONLY. apply_prior_tint, the per-palette _prior_session_amount dict, the slider (imgui.slider_float), and the TOML key (prior_session_amount: float) all use float throughout. No int(0.5 * 255) or any other integer truncation in the transform pipeline. The only int 0-255 values are the TOML inputs and imgui.ImVec4 component representation (which is already float). apply_color_grades_to_editor_palette is NOT in this track (see spec §1.1.1).",
"threading_constraint": "No new threads. The new helpers run on the render thread (the only thread that calls theme_2 helpers from the GUI). Persistence uses the existing app._flush_to_config() / save_config() flow.",
"user_requirement": "Default per-theme prior_session_amount = 0.3 (subtle). Slider in the Theme Settings panel (mirrors the existing Tone Mapping section). Per-palette state with per-palette TOML defaults. Falls back to scope (iii) (whole window tint) if scope (ii) doesn't look 'obviously old' in manual review.",
"verification_criteria": [
"All 3 new theme slots (prior_session_bg, prior_session_tint, prior_session_amount) present in ThemePalette, fallback dict, and all 8 themes/*.toml",
"_prior_session_amount per-palette dict implemented with get/set/reset; persists to config.toml; restores on next launch",
"apply_prior_tint: identity at 0.0, pure tint at 1.0, monotonic, alpha-preserved, all-float, all values in [0,1]",
"6 prior-session render sites in src/gui_2.py use prior_session_bg (not bubble_vendor); content is sepia-tinted via apply_prior_tint",
"Theme Settings panel has working 'Prior Session Sepia (Per-Palette)' section with slider 0.0-1.0 and Reset button",
"All new tests pass (test_prior_session_amount, test_prior_session_tint, test_prior_session_toml, test_prior_session_render, test_prior_session_persistence)",
"HONEST DISCLOSURE in final report: code-block tonemap-awareness is NOT fixed (upstream imgui_bundle 1.92.5 API does not expose per-instance Palette struct; only 4-value enum). Track ships without the code-block fix.",
"No regressions in 273+ existing live_gui tests (batch-verified, not isolation)",
"Manual smoke: prior-session look is 'obviously old' at default 0.3; slider scales smoothly (no integer stepping artifacts); value persists across restart; switching themes snaps to new default",
"No diagnostic stderr writes in production code (per AGENTS.md 'No Diagnostic Noise in Production' rule)",
"No git restore / git checkout -- / git reset without explicit user permission (HARD BAN per AGENTS.md)"
],
"links": {
"design_doc": "../../docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md",
"sibling_track": "../multi_themes_20260604/",
"predecessor_track": "../prior_session_test_harden_20260605/",
"related_docs": [
"docs/guide_themes.md",
"docs/guide_testing.md",
"docs/guide_architecture.md",
"docs/guide_app_controller.md"
]
}
}
@@ -0,0 +1,605 @@
# Track: Prior-Session Sepia Tint
**Status:** Active (planning)
**Initialized:** 2026-06-10
**Owner:** Tier 2 Tech Lead
**Priority:** C (UI polish; no functional blocker; complements `multi_themes_20260604` and the 2026-06-05 prior-session work)
**Parent track (none).** Sibling: `multi_themes_20260604` (added TOML theme loading + per-theme syntax palette mapping).
**Design doc:** [docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md](../../docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md)
---
## 1. Problem Statement
When a user enters Historical Replay mode (a.k.a. "prior session" view), the current implementation
uses `theme.get_color("bubble_vendor")` as the background tint at three call sites
(`src/gui_2.py:1028`, `:3960`, plus the "HISTORICAL VIEW" banners at `:5142` and `:5440`).
This is **not a dedicated prior-session color slot** — it is a semantic overload of the
"Vendor API" role's bubble color. Authors of new themes cannot tune the prior-session
feel without changing the Vendor API bubble.
Beyond the missing dedicated slot, there is no **content-level color grading**: text, markdown,
and code blocks inside prior-session views render at full saturation in the active palette.
The "I'm looking at a past session" cue is carried only by the bg tint, not by the content.
**Pre-existing related bug** (surfaced by the user during brainstorming): code blocks rendered
by `imgui_color_text_edit` (`src/markdown_helper.py:328-340`, `src/gui_2.py:4773-4780`) are **not
tonemap-aware**. Their colors are loaded from the library's hardcoded palettes (`dark`, `light`,
`mariana`, `retro_blue`) and 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.
### 1.1 Goals (in scope)
- **G1** — Add three new theme slots, per-palette: `prior_session_bg`, `prior_session_tint`, and
a per-palette float `prior_session_amount`. Defaults baked into `ThemePalette`
(`src/theme_models.py`) and the hardcoded fallback dict (`src/theme_2.py:184-201`).
- **G2** — Add a runtime state dict `_prior_session_amount: dict[str, float]` in `src/theme_2.py`
mirroring `_brightness/_contrast/_gamma` exactly. Expose
`get_prior_session_amount(palette) / set_prior_session_amount(palette, val) /
reset_prior_session(palette)`.
- **G3** — Add the pure-function helper `apply_prior_tint(rgba, palette_name)` in
`src/theme_2.py`. All math uses **float** (not int) end-to-end per user requirement.
- **G4** — Replace the 2 hardcoded `bubble_vendor` tint sites in `src/gui_2.py` (lines 1028, 3960)
with the new `prior_session_bg` slot, and wrap content-rendering color calls with
`apply_prior_tint(...)` at 4 prior-session sites (the 2 "HISTORICAL VIEW" banners plus
the comms/tool-log render functions that consume prior-session caches). Audit list in §4.1.
- **G5** — Add a new "Prior Session Sepia (Per-Palette)" section in the Theme Settings panel
(`src/gui_2.py:5007+`) with a single `slider_float` 0.01.0 and a "Reset" button. Mirrors
the existing Tone Mapping section's behavior: per-palette state, persisted to config, snaps
to per-palette default on theme switch.
- **G6** — Add TOML keys to all 8 existing `themes/*.toml` files with sensible per-theme defaults.
### 1.1.1 Honest constraint: code-block tonemap-awareness is NOT in scope
During spec self-review, the user and I verified the upstream `imgui_bundle 1.92.5` API:
`TextEditor.get_palette() -> PaletteId` and `TextEditor.set_palette(PaletteId) -> None` where
`PaletteId` is an enum with 4 hardcoded values (`dark`, `light`, `mariana`, `retro_blue`).
**There is no `Palette` struct with mutable per-color slots** in this API surface.
The user had hoped this track could fix the pre-existing "code blocks are not tonemap-aware"
bug. The honest answer is: **the upstream library does not expose a per-instance color
override API**, so we cannot apply `_tone_map` or `apply_prior_tint` to code-block syntax
token colors. The same constraint already forced the `multi_themes_20260604` track to ship
a `syntax_palette` field (one of 4 enums) rather than custom token colors.
What IS tonemap-aware in code blocks today: the bg, selection highlight, current-line fill,
and line number bg, because those use **ImGui style colors** which go through `get_color()`
and therefore through `_tone_map`. The four hardcoded syntax token colors
(`default`/`keyword`/`number`/`string`/`comment`/etc.) are NOT tonemap-aware and CANNOT
be made so without forking `imgui_bundle`.
This track does NOT fix that. **Out of scope** (see §1.2 N6 and §9 N1). The user should
not be promised a fix that the API doesn't support.
### 1.2 Non-Goals (out of scope)
- **N1** — Per-language syntax color customization beyond the existing 4 built-in palettes
(`dark`, `light`, `mariana`, `retro_blue`). Upstream `imgui_color_text_edit` limitation;
deferred per the `multi_themes_20260604` track.
- **N2** — A "film grain" or "vignette" post-effect to enhance the nostalgic feel. Pure
color-grading is sufficient for the "obvious prior session" cue. If the user wants more
later, that is a follow-up track.
- **N3** — Changing the prior-session default to per-theme *config* keys (e.g., users
overriding the default per-theme via `config.toml`). The slider value is saved as
per-palette state in the same way as brightness/contrast/gamma; the TOML key is the
*factory default*, not a user-tweakable value.
- **N4** — Applying the sepia transform to the **whole window** at scope (iii) of the
brainstorming. The user chose scope (ii) ("data + chrome inside prior-session views"),
with fallback to (iii) only if (ii) looks bad in manual review. Phase 3 includes a
manual smoke test that escalates to (iii) only if needed.
- **N5** — Moving `_prior_session_amount` into the same config dict as the existing
brightness/contrast/gamma state. Use the same persistence mechanism (config flush +
`app.save_config()`) but a separate dict so the "per-palette" semantics are explicit.
### 1.3 Design constraint (HARD — from user)
> "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(rgba, palette) -> tuple[float, float, float, float]` — float math
throughout (desaturation, lerp, alpha pass-through).
- The slider is `imgui.slider_float`, not `slider_int`. Range 0.01.0 inclusive.
- The `_prior_session_amount` dict stores `float`, not `Decimal` or `int`. Default 0.3.
- TOML key `prior_session_amount` is a `float`, not an int. Round-trip through
`tomllib`/`tomli_w` preserves the float type.
- `theme_2._tone_map()` is already float; the new transform composes on top of it.
- `apply_color_grades_to_editor_palette` iterates `Palette` color slots as float
quadruples (RGBA in 0.01.0).
The only place integer 0-255 RGB appears is the TOML theme files (existing convention).
That is the input boundary; the math happens entirely in float.
---
## 2. Current State Audit (as of commit `9f895117`)
### 2.1 Already Implemented (DO NOT re-implement)
- **TOML theme loading**: `src/theme_2.py:load_themes_from_disk()` (line 339),
`_TOML_PALETTES` / `_TOML_SEMANTIC_CACHE` / `_TOML_COLOUR_CACHE` (lines 64-67),
`get_color()` (line 153) which resolves through TOML → dataclass → fallback dict.
- **ThemePalette dataclass**: `src/theme_models.py:ThemePalette` (line ~86) with
`bubble_vendor: tuple[int, int, int] = (65, 55, 30)`. Schema validator at line 119.
- **Per-palette tonemap state**: `_brightness/_contrast/_gamma: dict[str, float]`
(`src/theme_2.py:75-77`), `_get_tm()` accessor (line 84), `set_brightness/contrast/gamma`
(lines 91-93), `reset_tone_mapping(palette)` (line 95).
- **Tonemap UI section**: `src/gui_2.py:5007-5023` — "Tone Mapping (Per-Palette)" header,
3 sliders + reset button. Each slider calls `theme.set_X(curr_palette, val)` then
`app._flush_to_config(); app.save_config()`.
- **Prior session state**: `AppController.is_viewing_prior_session: bool` (line 982),
`prior_session_entries` (line 983), `cb_exit_prior_session()` (line 2105).
`App.is_viewing_prior_session` is exposed via the App→Controller delegate.
- **Prior session render sites (6 audit-ready call sites)**:
- `src/gui_2.py:1027-1028``App._gui_func` window_bg wrap.
- `src/gui_2.py:3959-3961``render_prior_session_view` (dedicated prior view).
- `src/gui_2.py:4087-4193``render_comms_history_panel` consumes
`_comms_log_cache` which is replaced with `prior_session_entries` at line 1560
when `is_viewing_prior_session` is True.
- `src/gui_2.py:4591+``render_tool_calls_panel` consumes `_tool_log_cache`
similarly replaced at line 1568.
- `src/gui_2.py:5140-5145``render_mma_dashboard` "HISTORICAL VIEW" banner.
- `src/gui_2.py:5438-5442``render_tier_stream_panel` "HISTORICAL VIEW" banner.
- **TextEditor render sites (2)**:
- `src/markdown_helper.py:328-340` — per-code-block editor, `editor.set_palette(p_id)`.
- `src/gui_2.py:4773-4780` — text viewer TextEditor.
- **Syntax palette mapping**: `src/theme_2.py:350-367` `get_syntax_palette_for_theme` and
`apply_syntax_palette` use the upstream `ed.TextEditor.PaletteId` enum (4 values).
`apply_syntax_palette` is called from `apply()` (line 220, line 278) and on
`MarkdownRenderer.__init__` (line 90-91).
- **8 existing themes**: `themes/10x_dark.toml`, `themes/binks.toml`,
`themes/gruvbox_dark.toml`, `themes/monokai.toml`, `themes/moss.toml`,
`themes/nord_dark.toml`, `themes/solarized_dark.toml`, `themes/solarized_light.toml`.
Each defines a `bubble_vendor` line; none has prior-session slots.
- **Test infrastructure**: `tests/conftest.py` `live_gui`, `isolate_workspace`,
`reset_paths`, `reset_ai_client` fixtures. Puppeteer pattern documented in
`docs/guide_simulations.md`.
### 2.2 Gaps to Fill (This Track's Scope)
- **Gap A** — No dedicated prior-session theme slots. The bg is a reuse of `bubble_vendor`.
- **Gap B** — No content-level color grading. Text/markdown in prior views stays at full
palette saturation.
- **Gap C** — No per-palette runtime control over the prior-session amount. Authors can't
tune it without recompiling.
- **Gap D** — Code blocks (`TextEditor`) are not tonemap-aware (pre-existing). The
tonemap slider does not affect syntax-highlighted code. **NOT in scope for this track**
— see §1.1.1: the upstream `imgui_bundle 1.92.5` API does not expose per-instance color
override for `TextEditor.PaletteId` (the only API is the 4-value enum). Fixing this
requires forking the library or writing a custom syntax highlighter, which is a
separate track.
- **Gap E** — No `apply_prior_tint` helper exists. Any future "color grade any
ThemeColor" feature would reimplement this from scratch.
---
## 3. Approach: Per-render explicit transform (A1)
The brainstorming design doc (`docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md`)
records the full rationale. Summary:
- The view layer composes the color transform. `theme.get_color(name)` continues to return
the tonemap-graded but non-prior-tinted color. A new `apply_prior_tint(rgba, palette)`
helper applies the sepia blend on top. Prior-session render sites wrap their
`theme.get_color(...)` calls with `apply_prior_tint(...)` (one-line wrap, ~6 sites).
- The TextEditor palette fix uses a parallel `apply_color_grades_to_editor_palette(editor,
palette_name)` helper that **mutates the per-instance `Palette` struct in-place** and
re-applies it via `editor.set_palette(palette_struct)`. This is the only known way to
per-instance override the upstream library's hardcoded palette.
- The per-palette `_prior_session_amount` dict mirrors the existing tonemap dicts
(`_brightness`, `_contrast`, `_gamma`) exactly. The same persistence mechanism
(`app._flush_to_config(); app.save_config()`) applies.
### 3.1 Why not the alternatives
- **A2 (transparent via `get_color`)** — would auto-apply sepia when
`is_viewing_prior_session` is True. Rejected: violates the data-oriented principle
("the view composes"); risks accidentally tinting status indicators that must stay
saturated (e.g., the red error dot in `is_thinking`).
- **A3 (context manager scope)** — would require a `with prior_session_view():`
wrapper at every prior-session site. Rejected: more boilerplate than A1 with no
benefit.
### 3.2 Float-only math contract
The transform is `result = lerp(desaturate(input), tint_color, amount)`.
- `desaturate(rgba)` — float math: `luma = 0.2126*r + 0.7152*g + 0.0722*b` (BT.709),
`return (luma, luma, luma, a)`. All in 0.01.0.
- `lerp(a, b, t)` — float: `a + (b - a) * t` for each channel. The result is in 0.01.0.
- `apply_prior_tint(rgba, palette)` — `return lerp(desaturate(rgba), get_prior_tint(palette),
get_prior_session_amount(palette))`. Alpha passed through unchanged.
- `apply_color_grades_to_editor_palette(editor, palette_name)` — iterates the
`Palette` struct's color slots (RGBA float quadruples), calls
`_tone_map(slot, palette_name)` first, then conditionally `apply_prior_tint(slot,
palette_name)`. Sets the modified `Palette` back via `editor.set_palette(palette)`.
The user can confirm the per-palette amount via `imgui.slider_float("##prior_amt",
theme.get_prior_session_amount(curr_p), 0.0, 1.0, "%.2f")` with `0.0 <= amount <= 1.0`.
Default 0.3 (subtle, per user choice).
---
## 4. Functional Requirements
### 4.1 Theme model additions
`src/theme_models.py:ThemePalette` gains 3 fields:
```python
prior_session_bg: tuple[int, int, int] = (60, 50, 35) # dark default
prior_session_tint: tuple[int, int, int] = (112, 66, 20) # classic sepia
prior_session_amount: float = 0.3 # subtle
```
`src/theme_models.py:ThemeFile.from_dict()` and `to_dict()` (lines 137-157) round-trip
the new fields. The validator at line 119 enforces `0.0 <= prior_session_amount <= 1.0`
and 3-tuple RGB bounds.
`src/theme_2.py:184-201` fallback dict gains the 3 keys with the same defaults.
### 4.2 Theme module additions
`src/theme_2.py` gains, in order, after the existing tonemap state (~line 95):
```python
_prior_session_amount: dict[str, float] = {}
def _get_psa(palette: str, default: float) -> float:
return _prior_session_amount.get(palette, default)
def get_prior_session_amount(palette: str) -> float:
return _get_psa(palette, 0.3)
def set_prior_session_amount(palette: str, val: float) -> None:
val = max(0.0, min(1.0, float(val))) # clamp + cast
_prior_session_amount[palette] = val
def reset_prior_session(palette: str) -> None:
_prior_session_amount.pop(palette, None)
```
### 4.3 Transform helpers
`src/theme_2.py` gains, after the tonemap helpers (~line 110):
```python
def _desaturate(rgba: tuple[float, float, float, float]) -> tuple[float, float, float, float]:
"""Convert to grayscale using BT.709 luma. All float math."""
r, g, b, a = rgba
luma = 0.2126 * r + 0.7152 * g + 0.0722 * b
return (luma, luma, luma, a)
def _lerp_rgba(
a: tuple[float, float, float, float],
b: tuple[float, float, float, float],
t: float,
) -> tuple[float, float, float, float]:
"""Linear interpolation. All float math; t in [0.0, 1.0]."""
t = max(0.0, min(1.0, t))
return (
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
a[3] + (b[3] - a[3]) * t,
)
def apply_prior_tint(
rgba: tuple[float, float, float, float],
palette_name: str,
) -> tuple[float, float, float, float]:
"""Apply the per-palette prior-session sepia blend to a color.
result = lerp(desaturate(input), get_prior_tint(palette), get_prior_session_amount(palette))
No-op at amount=0.0; pure tint at amount=1.0.
"""
amount = get_prior_session_amount(palette_name)
if amount <= 0.0:
return rgba
tint_rgb = get_color("prior_session_tint")
tint = (tint_rgb.x, tint_rgb.y, tint_rgb.z, rgba[3])
return _lerp_rgba(_desaturate(rgba), tint, amount)
### 4.4 Call-site wraps in `src/gui_2.py`
The 6 prior-session-affected sites get the following surgical edits:
| Line | Current | New |
|---|---|---|
| 1027-1028 | `with imscope.style_color(imgui.Col_.window_bg, theme.get_color("bubble_vendor")):` | `with imscope.style_color(imgui.Col_.window_bg, theme.get_color("prior_session_bg")):` |
| 3960-3961 | `with imscope.style_color(imgui.Col_.child_bg, theme.get_color("bubble_vendor")):` | `with imscope.style_color(imgui.Col_.child_bg, theme.get_color("prior_session_bg")):` |
| 5142 | `c = theme.get_color("status_warning") if theme.is_nerv_active() else theme.get_color("status_warning")`<br>`imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY")` | wrap with `apply_prior_tint` per the call-site pattern in §4.3 |
| 5440 | Same pattern as 5142 | Same |
| `_render_comms_history_panel` (`4087-4193`) | Comms entries that iterate `log_to_render` (line 4115) | When `app.is_viewing_prior_session` is True, wrap each `theme.get_color()` call (for row text, role badge, etc.) with `apply_prior_tint` |
| `render_tool_calls_panel` (`4591+`) | Same | Same |
A small helper pair is added to `src/theme_2.py` (next to `apply_prior_tint`) to convert
`imgui.ImVec4` ↔ float tuple, so the wrap is one expression per call site:
```python
def _imvec4_to_rgba(v: imgui.ImVec4) -> tuple[float, float, float, float]:
return (float(v.x), float(v.y), float(v.z), float(v.w))
def _rgba_to_imvec4(rgba: tuple[float, float, float, float]) -> imgui.ImVec4:
return imgui.ImVec4(*rgba)
```
Call-site pattern (e.g., the line 5142 banner):
```python
c_raw = theme.get_color("status_warning")
if app.is_viewing_prior_session:
c = _rgba_to_imvec4(apply_prior_tint(_imvec4_to_rgba(c_raw), theme.get_current_palette()))
else:
c = c_raw
imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY")
```
### 4.5 Theme Settings panel UI
`src/gui_2.py:5007+` gains a new section after the "Reset Tone Mapping" button:
```python
imgui.separator()
imgui.text("Prior Session Sepia (Per-Palette)")
curr_p = theme.get_current_palette()
ch_p, p = imgui.slider_float("##ps_amount", theme.get_prior_session_amount(curr_p), 0.0, 1.0, "%.2f")
if ch_p:
theme.set_prior_session_amount(curr_p, p)
app._flush_to_config()
app.save_config()
if imgui.button("Reset Prior Session Sepia"):
theme.reset_prior_session(curr_p)
app._flush_to_config()
app.save_config()
```
Persistence: `_prior_session_amount` is saved to `[theme] prior_session_amount` in
`config.toml` keyed by palette, mirroring the existing brightness/contrast/gamma keys.
`load_from_config()` (`src/theme_2.py:319-335`) reads it back on startup.
### 4.7 Theme TOML files
All 8 `themes/*.toml` files gain the 3 new keys. Defaults:
| Theme | prior_session_bg | prior_session_tint | prior_session_amount |
|---|---:|---:|---:|
| 10x_dark | (60, 50, 35) | (112, 66, 20) | 0.3 |
| binks | (235, 220, 190) | (140, 80, 30) | 0.3 |
| gruvbox_dark | (60, 50, 35) | (112, 66, 20) | 0.3 |
| monokai | (60, 50, 35) | (112, 66, 20) | 0.3 |
| moss | (60, 50, 35) | (112, 66, 20) | 0.3 |
| nord_dark | (60, 50, 35) | (112, 66, 20) | 0.3 |
| solarized_dark | (60, 50, 35) | (112, 66, 20) | 0.3 |
| solarized_light | (235, 220, 190) | (140, 80, 30) | 0.3 |
The light themes use a cream bg (235, 220, 190) so the bg is distinct from the
text-bg and from the Vendor API bubble. The sepia tint on light themes is slightly
warmer (140, 80, 30) to maintain visible contrast on a light background.
---
## 5. Non-Functional Requirements
- **Performance** — `apply_prior_tint` is a pure function: 3 multiplies + 3 lerps + alpha
pass-through per call, all float. At ~6 prior-session call sites × ~10 calls per frame
in a 60Hz render, total CPU cost is <50µs/frame. Negligible. (`apply_color_grades_to_editor_palette`
is NOT in this track — see §1.1.1.)
- **Thread safety** — all new module-level state in `theme_2.py` is keyed by `palette: str`
and accessed in the render thread (the only thread that calls these helpers). No new
threads introduced.
- **Float precision** — All math is float. No integer truncation, no `int(0.5 * 255)`.
Output is `tuple[float, float, float, float]` consumed by `imgui.ImVec4` (which is
already float-based). The TOML round-trip for `prior_session_amount: float` is
tested in `tests/test_prior_session_toml.py`.
- **Backward compatibility** — Themes missing the new keys fall back to the defaults in
`src/theme_2.py:184-201`. Existing user themes (if any) continue to work; the new
keys are optional with sensible defaults.
- **Hot reload** — Adding a new theme key is not a hot-reload concern (themes are
loaded at startup). The new `_prior_session_amount` runtime state is not hot-reloaded
(same as the existing tonemap state).
---
## 6. Architecture Reference
- `docs/guide_architecture.md` — Threading model (the helpers run on the render thread).
- `docs/guide_themes.md` — Theme TOML schema, fallback dict, palette resolution.
- `docs/guide_testing.md` — `live_gui` fixture, Puppeteer pattern, structural testing
contract, audit-script policy.
- `docs/guide_app_controller.md` — `AppController._settable_fields` /
`_gettable_fields` registries (we add `prior_session_amount` to `_settable_fields`
if the user wants to inspect/override via the Hook API; otherwise it lives in
`theme_2` only).
- `docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md` — Brainstorm
design doc with full rationale for the A1 approach.
- `conductor/tracks/multi_themes_20260604/spec.md` — Sibling track that established
the TOML theme loading and per-theme syntax palette mapping pattern.
- `conductor/tracks/prior_session_test_harden_20260605/` — Predecessor that
refactored `test_prior_session_no_pop_imbalance` to call the narrow
`render_prior_session_view` (now a more testable function thanks to that work).
---
## 7. Phases
The track is small enough to ship in 3-4 phases over ~2 days. The TDD Red→Green→Refactor
discipline applies per the project workflow. Atomic per-task commits. Git notes attached
to each commit.
### Phase 1: Theme model + state + helpers (Day 1, ~3 hours)
- [ ] T1.1 (Red): `tests/test_prior_session_amount.py` — `get/set/reset_prior_session_amount`
with per-palette dict semantics; default 0.3 when key absent; reset removes the key;
set clamps to [0.0, 1.0].
- [ ] T1.2 (Green): add `_prior_session_amount` dict + accessors to `src/theme_2.py`.
- [ ] T1.3 (Red): `tests/test_prior_session_tint.py` — `apply_prior_tint` math:
- At amount=0.0, returns input unchanged.
- At amount=1.0, returns pure sepia tint (modulated by input alpha).
- Monotonic: amount=0.5 is between amount=0.0 and amount=1.0 outputs (in luma).
- All values in [0.0, 1.0] for any input.
- Alpha is preserved exactly.
- [ ] T1.4 (Green): add `_desaturate`, `_lerp_rgba`, `apply_prior_tint` to `src/theme_2.py`.
All math is float.
- [ ] T1.5 (Red): `tests/test_prior_session_toml.py` — round-trip a theme TOML with the
3 new keys; verify defaults when keys are missing; verify validation rejects
`prior_session_amount < 0.0` or `> 1.0` or non-3-tuple RGB.
- [ ] T1.6 (Green): add the 3 fields to `ThemePalette` (`src/theme_models.py`); update
`from_dict` / `to_dict` / `validator`; add the 3 keys to the fallback dict
(`src/theme_2.py:184-201`).
- [ ] T1.7: Add the 3 keys to all 8 `themes/*.toml` files.
- [ ] T1.8: Run `scripts/audit_weak_types.py` and `scripts/check_test_toml_paths.py`;
confirm no regressions.
- [ ] T1.9: Commit + git note.
### Phase 2: Call-site wraps (Day 1-2, ~4 hours)
- [ ] T2.1 (Red): `tests/test_prior_session_render.py` — unit test for the helper
`_imvec4_to_rgba` / `_rgba_to_imvec4` round-trip.
- [ ] T2.2 (Green): add the 2 helpers to `src/theme_2.py` (next to `apply_prior_tint`).
- [ ] T2.3: Refactor `src/gui_2.py:1027-1028` and `:3960-3961` to use
`theme.get_color("prior_session_bg")` (replacing `bubble_vendor`).
- [ ] T2.4: Wrap the 4 "HISTORICAL VIEW" + `_render_comms_history_panel` + `_render_tool_calls_panel`
call sites with `apply_prior_tint`. All edits are 1-3 lines per site.
- [ ] T2.5 (DROPPED): `tests/test_code_block_tonemap.py` — **not applicable.** The helper
was dropped per §1.1.1. The test is also dropped.
- [ ] T2.6 (DROPPED): `apply_color_grades_to_editor_palette` — **not implemented.**
See §1.1.1.
- [ ] T2.7 (DROPPED): TextEditor wrap into markdown_helper.py and gui_2.py — **not done.**
See §1.1.1.
- [ ] T2.8: Commit + git note.
### Phase 3: Theme panel UI + persistence (Day 2, ~2 hours)
- [ ] T3.1: Add the "Prior Session Sepia (Per-Palette)" section to
`src/gui_2.py:5007+` per §4.6. Mirrors the existing Tone Mapping section.
- [ ] T3.2: Wire `app._flush_to_config(); app.save_config()` on change.
- [ ] T3.3: Update `src/theme_2.py:save_to_config()` (line 301-317) to persist
`_prior_session_amount` under `[theme.prior_session_amount.<palette>]` (a nested
table mirroring the existing `[theme.tone_mapping.<palette>]` structure on line 312).
Example resulting TOML:
```toml
[theme]
palette = "10x Dark"
[theme.prior_session_amount]
"10x Dark" = 0.3
```
`load_from_config()` (line 319-335) reads it back the same way `_brightness` is
read on line 333.
- [ ] T3.4: Update `src/theme_2.py:load_from_config()` (line 319-335) to read it back.
- [ ] T3.5 (Red): `tests/test_prior_session_persistence.py` — slider change →
`app._flush_to_config(); app.save_config()` → restart (or call `load_from_config`)
→ state restored.
- [ ] T3.6 (Green): implement persistence in save/load.
- [ ] T3.7: Commit + git note.
### Phase 4: Verify + checkpoint (Day 2, ~2 hours)
- [ ] T4.1: Run the full `live_gui` test batch (per the existing
`live_gui_test_hardening_20260605` guidance — batch, not isolation). Confirm
no regressions in the 273+ existing tests.
- [ ] T4.2: Run the new tests in `tests/test_prior_session_*.py` and
`tests/test_code_block_tonemap.py`. All pass.
- [ ] T4.3: Manual smoke:
- Launch `uv run sloppy.py --enable-test-hooks`.
- Open Theme Settings. Find the new "Prior Session Sepia" section. Verify slider
snaps to the per-palette default (0.3).
- Enter a prior session (`Session Analysis → Open Prior Session`). Verify the
"HISTORICAL VIEW" banner is tinted, the prior discussion entries are tinted
(subtle sepia), and code blocks within them are also tinted.
- Adjust the slider to 0.0 → 1.0. Verify the tint amount scales smoothly (no
integer stepping artifacts).
- Switch themes (e.g., from `10x Dark` to `Solarized Light`). Verify the slider
snaps to the new palette's default.
- Quit and restart. Verify the slider value persists.
- If the prior-session chrome (banners, button labels) does NOT look "obviously
old" at amount=0.3, escalate to scope (iii) per brainstorming Q3 (b) — wrap
the entire `_gui_func` window bg with the prior-session tint instead of just
the prior-session views. Capture before/after screenshots in
`docs/reports/prior_session_sepia_<date>/`.
- [ ] T4.4: Phase checkpoint commit + git note with full verification report.
- [ ] T4.5: Update `conductor/tracks.md` with the new track entry (or remove
the entry if archival). Add `docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md`
to the planning-digest index (no digest exists yet for 2026-06-10; if one is
authored later, this spec should be referenced).
---
## 8. Verification Criteria
The track is complete when:
- [ ] All 3 new theme slots (`prior_session_bg`, `prior_session_tint`,
`prior_session_amount`) are present in `ThemePalette`, the fallback dict,
and all 8 `themes/*.toml` files.
- [ ] `_prior_session_amount` per-palette dict is implemented with get/set/reset
accessors, persists to `config.toml`, restores on next launch.
- [ ] `apply_prior_tint` passes the math contract: identity at 0.0, pure tint at 1.0,
monotonic, alpha-preserved, all-float output, all values in [0.0, 1.0].
- [ ] All 6 prior-session render sites in `src/gui_2.py` use `prior_session_bg` (not
`bubble_vendor`) and content is sepia-tinted via `apply_prior_tint`.
- [ ] Theme Settings panel has a working "Prior Session Sepia (Per-Palette)" section
with a 0.01.0 slider and a Reset button.
- [ ] All new tests pass: `tests/test_prior_session_amount.py`,
`tests/test_prior_session_tint.py`, `tests/test_prior_session_toml.py`,
`tests/test_prior_session_render.py`,
`tests/test_prior_session_persistence.py`. (`tests/test_code_block_tonemap.py`
is NOT created — the code-block fix is out of scope per §1.1.1.)
- [ ] No regressions in the 273+ existing `live_gui` tests (batch-verified per
`workflow.md` "Isolated-Pass Verification Fallacy" rule).
- [ ] Manual smoke confirms the prior-session look is "obviously old" at the
default 0.3 amount.
- [ ] **HONEST DISCLOSURE in the final track report**: the pre-existing "code
blocks are not tonemap-aware" bug is NOT fixed by this track. The
reason (upstream API constraint) is documented in §1.1.1 and §9.
- [ ] No diagnostic noise (`sys.stderr.write("[XYZ_DIAG] ...")`) in production
code. All instrumentation goes to `tests/artifacts/` log files.
- [ ] `git restore` / `git checkout --` / `git reset` are NOT used without
explicit user permission (HARD BAN per AGENTS.md).
---
## 9. Out of Scope (Definitively)
- **N1** — Per-language syntax color customization beyond the 4 built-in palettes.
Upstream limitation; deferred.
- **N2** — Film grain / vignette / scanline post-effect. Pure color-grading only.
- **N3** — Per-theme user overrides via `config.toml` (the TOML key is the factory
default; the slider is the user-tweakable value).
- **N4** — Process-isolation of the `apply_prior_tint` helper (it's pure; no need).
- **N5** — Reorganizing the existing `_brightness/_contrast/_gamma` state into a
single dict (out of scope; follow the existing pattern).
- **N6** — **Code-block tonemap-awareness.** The `imgui_bundle` 1.92.5 API
exposes `TextEditor.get_palette() -> PaletteId` (the 4-value enum: `dark`,
`light`, `mariana`, `retro_blue`) and `set_palette(PaletteId)`. There is
**no `Palette` struct with mutable per-color slots**. Per-instance
tonemap/sepia overrides on code-block syntax tokens are NOT possible
without forking the library. The pre-existing "code blocks not
tonemap-aware" behavior persists. Same constraint as the
`multi_themes_20260604` track, which shipped a per-theme `syntax_palette`
enum field rather than custom token colors.
- **N7** — Adding a `prior_session_indicator` color slot to the existing
`app_controller.py:1142` Hook API registry. The indicator is binary (on/off);
the new color is consumed by render code, not the Hook API.
---
## 10. Cross-References
- `conductor/tracks/multi_themes_20260604/spec.md` — sibling track (TOML theme
loading pattern this track extends).
- `conductor/tracks/prior_session_test_harden_20260605/` — predecessor that
refactored the prior-session test to call `render_prior_session_view` narrowly.
- `docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md` — full
brainstorming design rationale.
- `docs/guide_themes.md` — theme system reference.
- `docs/guide_testing.md` — `live_gui` batch-verification rule and structural
testing contract.
- `conductor/workflow.md` — TDD Red→Green→Refactor + atomic per-task commits +
Phase Completion Verification Protocol.
- `conductor/product-guidelines.md` §"Phase 5: Heavy Curation & Structural
Integrity" — float-only math, no shortcuts, no integer truncation.
@@ -0,0 +1,148 @@
# 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