diff --git a/conductor/tracks/prior_session_sepia_20260610/metadata.json b/conductor/tracks/prior_session_sepia_20260610/metadata.json new file mode 100644 index 00000000..1e8ecd4d --- /dev/null +++ b/conductor/tracks/prior_session_sepia_20260610/metadata.json @@ -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" + ] + } +} diff --git a/conductor/tracks/prior_session_sepia_20260610/spec.md b/conductor/tracks/prior_session_sepia_20260610/spec.md new file mode 100644 index 00000000..02cc6e07 --- /dev/null +++ b/conductor/tracks/prior_session_sepia_20260610/spec.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.0–1.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.0–1.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.0–1.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.0–1.0. +- `lerp(a, b, t)` — float: `a + (b - a) * t` for each channel. The result is in 0.0–1.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")`
`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.]` (a nested + table mirroring the existing `[theme.tone_mapping.]` 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_/`. +- [ ] 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.0–1.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. diff --git a/docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md b/docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md new file mode 100644 index 00000000..272807f6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-10-prior-session-sepia-design.md @@ -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