From 2ce299f2935a23fbf499afff5194e42564332aab Mon Sep 17 00:00:00 2001 From: Fimeg Date: Fri, 15 May 2026 17:40:09 -0400 Subject: [PATCH] feat(tui): atmosphere restore from system/preferences/visual + archive resolved tasks Adds Atmosphere::from_name() constructor and wires a readback of system/preferences/visual.md at TUI conversation startup (alongside the existing energy-balance read), so the agent's last explicit atmosphere choice survives restarts. Archives 5 resolved task docs: - tui-clean-conversation-switch (switch_pending state machine in place) - scope-4-n25-reflection (408-line engine, fires at N+25) - tui-presence-and-interrupt (interjection queue, /btw, raise-hand, phase display, self-awareness pulse all built) - presence-visual-evolution (ChatPalette wired, posture-shift fix, cross-session atmosphere restore now landed) - presence-autonomy (atmosphere tool bidirectional, prefs readback now wired; from_posture() coupling remains per proto task scope) Updates CLAUDE.md active task queue to reflect current state. --- src/ui/app.rs | 23 +++++++++++++++++++++++ src/ui/atmosphere.rs | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/ui/app.rs b/src/ui/app.rs index 92e10cb..62b0ece 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1431,6 +1431,29 @@ impl App { self.rgp_portrait = crate::ui::rgp::load_portrait_glb(&assets); } + // Read the agent's last explicit atmosphere from + // system/preferences/visual.md and restore the chrome. The agent + // writes this when she uses the atmosphere tool; the TUI reads it + // at conversation start so her choice survives restarts. + let pref_path = repo.root().join("system").join("preferences").join("visual.md"); + if let Ok(content) = std::fs::read_to_string(&pref_path) { + if let Some(body) = content.strip_prefix("---\n") { + if let Some(end) = body.find("\n---\n") { + for line in body[..end].lines() { + if let Some((key, val)) = line.split_once(':') { + let key = key.trim(); + let val = val.trim().trim_matches('"'); + if key == "atmosphere" { + if let Some(atm) = crate::ui::atmosphere::Atmosphere::from_name(val) { + self.presence.transition_atmosphere(atm); + } + } + } + } + } + } + } + // Read energy balance from the agent's memfs. The file is written // by the backend after every turn (write_energy_balance in local.rs). // Parse the YAML frontmatter for generative/consumptive counts and diff --git a/src/ui/atmosphere.rs b/src/ui/atmosphere.rs index 8108f80..f8dfb6a 100644 --- a/src/ui/atmosphere.rs +++ b/src/ui/atmosphere.rs @@ -145,6 +145,29 @@ impl Atmosphere { } } + /// Parse an atmosphere from a preset name (case-insensitive, underscore-tolerant). + /// Returns `None` for unknown names and empty strings. + pub fn from_name(name: &str) -> Option { + let key = name.to_lowercase().replace(' ', "_"); + match key.as_str() { + "" | "default" => Some(Atmosphere::Default), + "mint_tea" => Some(Atmosphere::MintTea), + "therapeutic_blue" => Some(Atmosphere::TherapeuticBlue), + "lavender_calm" => Some(Atmosphere::LavenderCalm), + "warm_amber" => Some(Atmosphere::WarmAmber), + "peach_sunset" => Some(Atmosphere::PeachSunset), + "autumn_browns" => Some(Atmosphere::AutumnBrowns), + "neon_glow" => Some(Atmosphere::NeonGlow), + "aurora_borealis" => Some(Atmosphere::AuroraBorealis), + "cherry_blossom" => Some(Atmosphere::CherryBlossom), + "ocean_depths" => Some(Atmosphere::OceanDepths), + "midnight_galaxy" => Some(Atmosphere::MidnightGalaxy), + "twilight_mist" => Some(Atmosphere::TwilightMist), + "forest_greens" => Some(Atmosphere::ForestGreens), + _ => None, + } + } + /// Map a posture to a default atmosphere (when none is explicitly set). pub fn from_posture(posture: crate::ui::presence::Posture) -> Self { use crate::ui::presence::Posture;