use std::io;
use crate::panel::Key;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use newt_core::cognition::{cli_cognition, CognitionOverride};
use newt_core::initiative::{cli_initiative, Initiative};
use newt_core::role_profile::{Cognition, PersonalityLevel, PersonalityTrait, PersonalityTraits};
use newt_core::tenacity::{cli_tenacity, Tenacity};
fn cognition_ladder() -> Vec<CognitionOverride> {
let mut v = vec![CognitionOverride::Unset, CognitionOverride::Off];
v.extend(Cognition::all().into_iter().map(CognitionOverride::Set));
v
}
fn ladder<T: Copy>(levels: impl IntoIterator<Item = T>) -> Vec<Option<T>> {
std::iter::once(None)
.chain(levels.into_iter().map(Some))
.collect()
}
const NONE: &str = "none";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Row {
Persona,
Model,
Cognition,
Tenacity,
Initiative,
Personality(PersonalityTrait),
}
const ROWS: [Row; 10] = [
Row::Persona,
Row::Model,
Row::Cognition,
Row::Tenacity,
Row::Initiative,
Row::Personality(PersonalityTrait::Agreeableness),
Row::Personality(PersonalityTrait::Extraversion),
Row::Personality(PersonalityTrait::Warmth),
Row::Personality(PersonalityTrait::Approachability),
Row::Personality(PersonalityTrait::ProsocialBehavior),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ModelChoice {
pub(crate) name: String,
pub(crate) tag: String,
}
pub(crate) struct PanelSeed {
pub via: &'static str,
pub personas: Vec<PersonaChoice>,
pub current_persona: Option<String>,
pub backend: Option<String>,
pub base_initiative: Initiative,
pub initiative_from_family: bool,
pub models: Option<Vec<ModelChoice>>,
pub current_model: String,
pub personality: PersonalityTraits,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Dial<T> {
Inherit(T),
Set(T),
}
impl<T: Copy> Dial<T> {
pub(crate) fn value(self) -> T {
match self {
Self::Inherit(v) | Self::Set(v) => v,
}
}
pub(crate) fn is_dirty(self) -> bool {
matches!(self, Self::Set(_))
}
pub(crate) fn set(&mut self, v: T) {
*self = Self::Set(v);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Mode {
Normal,
Command(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PersonaChoice {
pub name: String,
pub profile: Option<newt_core::RoleProfile>,
}
impl From<crate::Persona> for PersonaChoice {
fn from(persona: crate::Persona) -> Self {
Self {
name: persona.name,
profile: Some(persona.profile),
}
}
}
impl PersonaChoice {
pub(crate) fn for_panel(
loaded: Vec<crate::Persona>,
active: Option<&crate::Persona>,
) -> Vec<Self> {
let mut choices: Vec<Self> = loaded.into_iter().map(Self::from).collect();
if let Some(active) = active {
if let Some(choice) = choices.iter_mut().find(|p| p.name == active.name) {
*choice = active.clone().into();
} else {
choices.push(active.clone().into());
}
}
choices
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PersonaAction {
Keep,
Clear,
Switch(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SaveResult {
Saved { name: String },
Exists { name: String },
InvalidName(String),
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PanelOutcome {
Cancelled,
Applied {
persona: PersonaAction,
model: Option<String>,
personality: Vec<(PersonalityTrait, Option<PersonalityLevel>)>,
},
Saved { name: String },
SavedAndApplied {
name: String,
persona: PersonaAction,
model: Option<String>,
personality: Vec<(PersonalityTrait, Option<PersonalityLevel>)>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PanelState {
via: &'static str,
sel: usize,
persona_opts: Vec<String>,
personas: Vec<PersonaChoice>,
persona_idx: usize,
current_persona: Option<String>,
cognition: Dial<CognitionOverride>,
tenacity: Dial<Option<Tenacity>>,
initiative: Dial<Option<Initiative>>,
personality: [Dial<Option<PersonalityLevel>>; 5],
model_opts: Option<Vec<ModelChoice>>,
model: Dial<usize>,
current_model: String,
base_initiative: Initiative,
initiative_from_family: bool,
base_crew: bool,
backend: Option<String>,
mode: Mode,
status: Option<String>,
saved: Option<String>,
}
impl PanelState {
pub(crate) fn new(seed: PanelSeed) -> Self {
let PanelSeed {
via,
mut personas,
current_persona,
backend,
base_initiative,
initiative_from_family,
models,
current_model,
personality,
} = seed;
let current_model = current_model.as_str();
let model_opts = models
.map(|mut list| {
if !current_model.is_empty() && !list.iter().any(|m| m.name == current_model) {
list.push(ModelChoice {
name: current_model.to_string(),
tag: "(not served)".to_string(),
});
}
list
})
.filter(|list| !list.is_empty());
let model_idx = model_opts
.as_ref()
.and_then(|list| list.iter().position(|m| m.name == current_model))
.unwrap_or(0);
if let Some(cur) = ¤t_persona {
if !personas.iter().any(|p| &p.name == cur) {
personas.push(PersonaChoice {
name: cur.clone(),
profile: None,
});
}
}
let mut persona_opts = Vec::with_capacity(personas.len() + 1);
persona_opts.push(NONE.to_string());
persona_opts.extend(personas.iter().map(|p| p.name.clone()));
let persona_idx = current_persona
.as_ref()
.and_then(|c| persona_opts.iter().position(|n| n == c))
.unwrap_or(0);
Self {
via,
sel: 0,
persona_opts,
personas,
persona_idx,
current_persona,
cognition: Dial::Inherit(cli_cognition()),
tenacity: Dial::Inherit(cli_tenacity()),
initiative: Dial::Inherit(cli_initiative()),
personality: PersonalityTrait::ALL.map(|kind| Dial::Inherit(personality.get(kind))),
model_opts,
model: Dial::Inherit(model_idx),
current_model: current_model.to_string(),
base_initiative,
initiative_from_family,
base_crew: std::env::var("NEWT_TEAM").is_ok(),
backend,
mode: Mode::Normal,
status: None,
saved: None,
}
}
fn in_command(&self) -> bool {
matches!(self.mode, Mode::Command(_))
}
pub(crate) fn down(&mut self) {
self.status = None;
self.sel = (self.sel + 1) % ROWS.len();
}
pub(crate) fn up(&mut self) {
self.status = None;
self.sel = (self.sel + ROWS.len() - 1) % ROWS.len();
}
pub(crate) fn cycle(&mut self, dir: i32) {
self.status = None;
match ROWS[self.sel] {
Row::Persona => {
self.persona_idx = clamp_step(self.persona_idx, dir, self.persona_opts.len());
}
Row::Model => {
if let Some(opts) = &self.model_opts {
self.model
.set(clamp_step(self.model.value(), dir, opts.len()));
}
}
Row::Cognition => {
let ladder = cognition_ladder();
let i = ladder
.iter()
.position(|o| *o == self.cognition.value())
.unwrap_or(0);
self.cognition.set(ladder[clamp_step(i, dir, ladder.len())]);
}
Row::Tenacity => {
let ladder = ladder(Tenacity::all());
let i = ladder
.iter()
.position(|t| *t == self.tenacity.value())
.unwrap_or(0);
self.tenacity.set(ladder[clamp_step(i, dir, ladder.len())]);
}
Row::Initiative => {
let ladder = ladder(Initiative::all());
let i = ladder
.iter()
.position(|t| *t == self.initiative.value())
.unwrap_or(0);
self.initiative
.set(ladder[clamp_step(i, dir, ladder.len())]);
}
Row::Personality(kind) => {
let (_, dial) = PersonalityTrait::ALL
.into_iter()
.zip(self.personality.iter_mut())
.find(|(candidate, _)| *candidate == kind)
.expect("every personality row has one dial");
let at = dial.value().map_or(0, |level| usize::from(level.get()) + 1);
let next = clamp_step(at, dir, 102); dial.set((next > 0).then(|| {
PersonalityLevel::try_from((next - 1) as u8)
.expect("clamped personality position")
}));
}
}
}
pub(crate) fn apply(&self) {
if self.cognition.is_dirty() {
let _ = crate::settings_form::apply_cognition(self.cognition.value(), self.via);
}
if self.tenacity.is_dirty() {
let _ = crate::settings_form::apply_tenacity(self.tenacity.value(), self.via);
}
if self.initiative.is_dirty() {
let _ = crate::settings_form::apply_initiative(self.initiative.value(), self.via);
}
}
pub(crate) fn persona_action(&self) -> PersonaAction {
let selected = &self.persona_opts[self.persona_idx];
if selected == NONE {
if self.current_persona.is_some() {
PersonaAction::Clear
} else {
PersonaAction::Keep
}
} else if Some(selected) == self.current_persona.as_ref() {
PersonaAction::Keep
} else {
PersonaAction::Switch(selected.clone())
}
}
pub(crate) fn is_noop(&self) -> bool {
!self.cognition.is_dirty()
&& !self.tenacity.is_dirty()
&& !self.initiative.is_dirty()
&& !self.model.is_dirty()
&& !self.personality.iter().any(|dial| dial.is_dirty())
&& self.persona_action() == PersonaAction::Keep
}
pub(crate) fn chosen_model(&self) -> Option<String> {
if !self.model.is_dirty() {
return None;
}
self.model_opts
.as_ref()
.and_then(|opts| opts.get(self.model.value()))
.map(|m| m.name.clone())
}
fn chosen_personality(&self) -> Vec<(PersonalityTrait, Option<PersonalityLevel>)> {
PersonalityTrait::ALL
.into_iter()
.zip(self.personality)
.filter_map(|(kind, dial)| match dial {
Dial::Set(level) => Some((kind, level)),
Dial::Inherit(_) => None,
})
.collect()
}
fn model_label(&self) -> String {
let Some(opts) = &self.model_opts else {
return "(backend unreachable — /models when it's back)".to_string();
};
let m = &opts[self.model.value()];
let tag = if m.tag.is_empty() {
String::new()
} else {
format!(" {}", m.tag)
};
if m.name == self.current_model {
format!("{}{tag} (active)", m.name)
} else {
format!("{}{tag} (pending)", m.name)
}
}
fn selected_persona(&self) -> Option<&PersonaChoice> {
if self.persona_idx == 0 {
None
} else {
self.personas.get(self.persona_idx - 1)
}
}
fn selected_profile(&self) -> Option<&newt_core::RoleProfile> {
self.selected_persona().and_then(|p| p.profile.as_ref())
}
fn projected_cognition(&self) -> Option<Cognition> {
match self.cognition.value() {
CognitionOverride::Set(c) => Some(c),
CognitionOverride::Off => None,
CognitionOverride::Unset => self.selected_profile().and_then(|p| p.cognition),
}
}
fn projected_tenacity(&self) -> Tenacity {
self.tenacity.value().unwrap_or_default()
}
fn chosen_initiative(&self) -> Option<Initiative> {
self.initiative
.value()
.or_else(|| self.selected_profile().and_then(|p| p.initiative))
}
fn projected_initiative(&self) -> Initiative {
self.chosen_initiative().unwrap_or(self.base_initiative)
}
fn personality_overrides(&self) -> PersonalityTraits {
let clears_conversation = self.persona_action() == PersonaAction::Clear;
let mut traits = PersonalityTraits::default();
for (kind, dial) in PersonalityTrait::ALL.into_iter().zip(self.personality) {
if dial.is_dirty() || !clears_conversation {
traits.set(kind, dial.value());
}
}
traits
}
fn projected_personality(&self) -> PersonalityTraits {
self.personality_overrides()
.resolve(self.selected_profile().and_then(|p| p.personality))
}
fn projected_backend(&self) -> Option<String> {
self.selected_profile()
.and_then(|p| p.backend.clone())
.or_else(|| self.backend.clone())
}
fn projected_crew(&self) -> bool {
self.selected_profile()
.and_then(|p| p.crew)
.unwrap_or(self.base_crew)
}
fn begin_command(&mut self, prefill: &str) {
self.status = None;
self.mode = Mode::Command(prefill.to_string());
}
fn command_char(&mut self, c: char) {
if let Mode::Command(buf) = &mut self.mode {
buf.push(c);
}
}
fn command_backspace(&mut self) {
if let Mode::Command(buf) = &mut self.mode {
buf.pop();
}
}
fn cancel_command(&mut self) {
self.mode = Mode::Normal;
}
fn run_command(
&mut self,
persist: &mut dyn FnMut(&str, &str, bool) -> SaveResult,
) -> Option<bool> {
let cmd = match &self.mode {
Mode::Command(buf) => buf.trim().to_string(),
Mode::Normal => return None,
};
self.mode = Mode::Normal;
let mut it = cmd.split_whitespace();
let raw = it.next().unwrap_or("");
let overwrite = raw.ends_with('!');
let verb = raw.trim_end_matches('!');
let name = it.next();
match verb {
"" => None,
"q" => Some(false),
"w" => {
self.try_save(name, overwrite, persist);
None
}
"wq" | "x" => {
if self.try_save(name, overwrite, persist) {
Some(true)
} else {
None }
}
other => {
self.status = Some(format!("unknown command ':{other}' (:w <name> | :wq | :q)"));
None
}
}
}
fn try_save(
&mut self,
name: Option<&str>,
overwrite: bool,
persist: &mut dyn FnMut(&str, &str, bool) -> SaveResult,
) -> bool {
let name = sanitize_name(name.unwrap_or(""));
if name.is_empty() {
self.status = Some("save needs a name: :w <name>".to_string());
return false;
}
let content = match self.persona_content(&name) {
Ok(content) => content,
Err(err) => {
self.status = Some(format!("save failed: {err}"));
return false;
}
};
match persist(&name, &content, overwrite) {
SaveResult::Saved { name } => {
self.status = Some(format!("saved persona '{name}'"));
self.saved = Some(name);
true
}
SaveResult::Exists { name } => {
self.status = Some(format!("'{name}' exists — :w! / :wq! to overwrite"));
false
}
SaveResult::InvalidName(msg) => {
self.status = Some(format!("invalid name: {msg}"));
false
}
SaveResult::Failed(err) => {
self.status = Some(format!("save failed: {err}"));
false
}
}
}
fn cognition_for_save(&self) -> Option<Cognition> {
self.projected_cognition()
}
fn persona_content(&self, name: &str) -> anyhow::Result<String> {
let mut profile = match self.selected_persona() {
Some(persona) => persona.profile.clone().ok_or_else(|| {
anyhow::anyhow!(
"persona '{}' is unavailable; its unknown profile cannot be saved",
persona.name
)
})?,
None => newt_core::RoleProfile {
role: Some(name.to_string()),
prompt: format!(
"# {name}\n\nSaved from the psyche panel — the dials above define this persona's posture."
),
..Default::default()
},
};
profile.backend = self.projected_backend();
profile.cognition = self.cognition_for_save();
profile.initiative = self.chosen_initiative();
profile.crew = profile.crew.or_else(|| self.base_crew.then_some(true));
let personality = self.projected_personality();
if personality != PersonalityTraits::default() || profile.personality.is_some() {
profile.personality = Some(personality);
}
profile.to_markdown()
}
fn cognition_cell(&self) -> (String, String) {
match self.cognition.value() {
CognitionOverride::Set(c) => (c.label().to_string(), "override".to_string()),
CognitionOverride::Off => ("off".to_string(), "override".to_string()),
CognitionOverride::Unset => {
let proj = self.projected_cognition();
let val = format!("auto → {}", proj.map_or("off", Cognition::label));
(val, self.inherit_provenance(proj.is_some()))
}
}
}
fn tenacity_cell(&self) -> (String, String) {
match self.tenacity.value() {
Some(t) => (t.label().to_string(), "override".to_string()),
None => (
format!("auto → {}", self.projected_tenacity().label()),
"base".to_string(),
),
}
}
fn initiative_cell(&self) -> (String, String) {
match self.initiative.value() {
Some(i) => (i.label().to_string(), "override".to_string()),
None => {
let val = format!("auto → {}", self.projected_initiative().label());
let from_persona = self.chosen_initiative().is_some();
let provenance = if !from_persona && self.initiative_from_family {
"model default".to_string()
} else {
self.inherit_provenance(from_persona)
};
(val, provenance)
}
}
}
fn inherit_provenance(&self, from_persona: bool) -> String {
match (from_persona, self.selected_persona()) {
(true, Some(p)) => format!("persona: {}", p.name),
_ => "base".to_string(),
}
}
fn persona_label(&self) -> String {
let name = &self.persona_opts[self.persona_idx];
if Some(name) == self.current_persona.as_ref() {
format!("{name} (active)")
} else if name == NONE {
if self.current_persona.is_some() {
"none (clears active)".to_string()
} else {
"none".to_string()
}
} else {
format!("{name} (pending)")
}
}
fn view_rows(&self) -> Vec<RowView> {
let (cog_val, cog_prov) = self.cognition_cell();
let (ten_val, ten_prov) = self.tenacity_cell();
let (ini_val, ini_prov) = self.initiative_cell();
let mut rows = vec![
RowView {
label: "persona",
value: self.persona_label(),
provenance: String::new(),
selected: ROWS[self.sel] == Row::Persona,
editable: true,
},
RowView {
label: "model",
value: self.model_label(),
provenance: if self.model_opts.is_some() {
"active backend".to_string()
} else {
String::new()
},
selected: ROWS[self.sel] == Row::Model,
editable: self.model_opts.is_some(),
},
RowView {
label: "cognition",
value: cog_val,
provenance: cog_prov,
selected: ROWS[self.sel] == Row::Cognition,
editable: true,
},
RowView {
label: "tenacity",
value: ten_val,
provenance: ten_prov,
selected: ROWS[self.sel] == Row::Tenacity,
editable: true,
},
RowView {
label: "initiative",
value: ini_val,
provenance: ini_prov,
selected: ROWS[self.sel] == Row::Initiative,
editable: true,
},
RowView {
label: "provider",
value: self.projected_backend().unwrap_or_else(|| "—".to_string()),
provenance: self.backend_provenance(),
selected: false,
editable: false,
},
RowView {
label: "crew",
value: format!(
"{} (launch gate)",
if self.projected_crew() { "on" } else { "off" }
),
provenance: self.crew_provenance(),
selected: false,
editable: false,
},
];
let overrides = self.personality_overrides();
let projected = self.projected_personality();
let traits = PersonalityTrait::ALL.into_iter().map(|kind| {
let (value, provenance) = match overrides.get(kind) {
Some(level) => (format!("{}/100", level.get()), "override".into()),
None => (
format!(
"auto → {}",
projected.get(kind).map_or_else(
|| "unspecified".into(),
|level| format!("{}/100", level.get())
)
),
self.inherit_provenance(projected.get(kind).is_some()),
),
};
RowView {
label: kind.label(),
value,
provenance,
selected: ROWS[self.sel] == Row::Personality(kind),
editable: true,
}
});
let at = rows
.iter()
.position(|r| r.label == "provider")
.unwrap_or(rows.len());
rows.splice(at..at, traits);
rows
}
fn backend_provenance(&self) -> String {
match self.selected_persona() {
Some(p) if p.profile.as_ref().is_some_and(|p| p.backend.is_some()) => {
format!("persona: {}", p.name)
}
_ => "base".to_string(),
}
}
fn crew_provenance(&self) -> String {
match self.selected_persona() {
Some(p) if p.profile.as_ref().is_some_and(|p| p.crew.is_some()) => {
format!("declaration: {}; applies next launch", p.name)
}
_ => "current launch gate".to_string(),
}
}
}
pub(crate) struct RowView {
pub(crate) label: &'static str,
pub(crate) value: String,
pub(crate) provenance: String,
pub(crate) selected: bool,
pub(crate) editable: bool,
}
fn sanitize_name(s: &str) -> String {
s.trim()
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
pub(crate) fn clamp_step(i: usize, dir: i32, len: usize) -> usize {
let Some(last) = len.checked_sub(1) else {
return 0;
};
let n = i32::try_from(last).unwrap_or(i32::MAX);
(i32::try_from(i).unwrap_or(i32::MAX) + dir).clamp(0, n) as usize
}
pub(crate) const PANEL_HEIGHT: u16 = ROWS.len() as u16 + 5;
fn draw(f: &mut ratatui::Frame, state: &PanelState) {
let bottom = if let Mode::Command(buf) = &state.mode {
command_line(buf)
} else if let Some(status) = &state.status {
status_line(status)
} else {
hint_line(
"↑↓ select · ←→ change (auto=inherit) · Enter apply · Esc cancel · Ctrl-S/:w <name> save",
)
};
render_panel(
f,
" psyche — operator dials ",
&state.view_rows(),
bottom,
19,
26,
);
}
pub(crate) fn render_panel(
f: &mut ratatui::Frame,
title: &str,
rows: &[RowView],
bottom: Line,
label_w: usize,
val_w: usize,
) {
render_panel_with_styles(f, title, rows, bottom, label_w, val_w, |_| {
(Style::default(), Style::default())
});
}
pub(crate) fn render_panel_with_styles(
f: &mut ratatui::Frame,
title: &str,
rows: &[RowView],
bottom: Line,
label_w: usize,
val_w: usize,
styles: impl Fn(&RowView) -> (Style, Style),
) {
let inner = crate::modal::frame(
f,
f.area(),
&crate::modal::Chrome {
title: title.trim(),
..crate::modal::Chrome::default()
},
);
let visible = usize::from(inner.height.saturating_sub(1));
let selected = rows.iter().position(|row| row.selected).unwrap_or(0);
let top = crate::list_cursor::ListCursor::new(rows.len(), visible, selected).top();
let mut lines: Vec<Line> = Vec::new();
for row in rows.iter().skip(top).take(visible) {
let marker = if row.selected { "❯ " } else { " " };
let name = format!("{marker}{:<label_w$}", row.label);
let val = if row.selected && row.editable {
format!("‹ {} ›", row.value)
} else {
row.value.clone()
};
let (name_style, val_style) = row_styles(row.selected, row.editable);
let (value_override, annotation_override) = styles(row);
let val_style = val_style.patch(value_override);
let mut spans = vec![
Span::styled(name, name_style),
Span::styled(format!("{val:<val_w$}"), val_style),
];
if !row.provenance.is_empty() {
spans.push(Span::styled(
row.provenance.clone(),
crate::theme::style(crate::theme::Role::Dim).patch(annotation_override),
));
}
lines.push(Line::from(spans));
}
lines.resize(visible, Line::default());
lines.push(bottom);
render_scroll_gutter(f, inner, rows.len(), top, visible);
let para = Paragraph::new(lines);
f.render_widget(
para,
Rect {
x: inner.x + 1,
y: inner.y,
width: inner.width.saturating_sub(1),
height: inner.height,
},
);
}
fn render_scroll_gutter(
f: &mut ratatui::Frame,
inner: Rect,
len: usize,
top: usize,
visible: usize,
) {
use crate::scrollbar::{SCROLL_ABOVE, SCROLL_BELOW, SCROLL_THUMB, SCROLL_TRACK};
if len <= visible || visible == 0 {
return;
}
let thumb = crate::scrollbar::thumb_row(top, len - visible, visible);
let glyphs: Vec<Line> = (0..visible)
.map(|row| {
let glyph = if row == 0 && top > 0 {
SCROLL_ABOVE
} else if row + 1 == visible && top + visible < len {
SCROLL_BELOW
} else if Some(row) == thumb {
SCROLL_THUMB
} else {
SCROLL_TRACK
};
Line::from(Span::styled(
glyph.to_string(),
crate::theme::style(crate::theme::Role::Dim),
))
})
.collect();
f.render_widget(
Paragraph::new(glyphs),
Rect {
height: u16::try_from(visible).unwrap_or(inner.height),
width: 1,
..inner
},
);
}
pub(crate) fn command_line(buf: &str) -> Line<'static> {
Line::from(Span::styled(
format!(":{buf}▏"),
crate::theme::style(crate::theme::Role::Ok),
))
}
pub(crate) fn status_line(status: &str) -> Line<'static> {
Line::from(Span::styled(
status.to_string(),
crate::theme::style(crate::theme::Role::Identity),
))
}
pub(crate) fn hint_line(hint: &'static str) -> Line<'static> {
Line::from(Span::styled(
format!(
"{hint} · ⇧↑↓ size · {} ? keys",
crate::prefix::active_label()
),
crate::theme::style(crate::theme::Role::Dim),
))
}
pub(crate) fn row_styles(selected: bool, editable: bool) -> (Style, Style) {
if selected {
(
crate::theme::style(crate::theme::Role::SelectedLabel),
crate::theme::style(crate::theme::Role::SelectedValue),
)
} else if editable {
(
crate::theme::style(crate::theme::Role::Text),
crate::theme::style(crate::theme::Role::Text),
)
} else {
(
crate::theme::style(crate::theme::Role::Dim),
crate::theme::style(crate::theme::Role::Dim),
)
}
}
pub(crate) fn run(
seed: PanelSeed,
persist: impl FnMut(&str, &str, bool) -> SaveResult,
window: Option<crate::session_worker::PanelWindow>,
) -> io::Result<PanelOutcome> {
let mut screen = PsycheScreen {
state: PanelState::new(seed),
persist,
};
let applied = crate::panel::drive(&mut screen, PANEL_HEIGHT, window.as_ref())?;
Ok(close_outcome(applied, &screen.state))
}
struct PsycheScreen<P: FnMut(&str, &str, bool) -> SaveResult> {
state: PanelState,
persist: P,
}
impl<P: FnMut(&str, &str, bool) -> SaveResult> crate::panel::Screen for PsycheScreen<P> {
fn draw(&self, frame: &mut ratatui::Frame) {
draw(frame, &self.state);
}
fn key(&mut self, key: Key) -> crate::panel::Flow {
use crate::panel::Flow;
if self.state.in_command() {
match key {
Key::Char(c) => self.state.command_char(c),
Key::Backspace => self.state.command_backspace(),
Key::Esc => self.state.cancel_command(),
Key::Enter => {
if let Some(apply) = self.state.run_command(&mut self.persist) {
return Flow::Close(apply);
}
}
_ => {}
}
return Flow::Stay;
}
match key {
Key::Up => self.state.up(),
Key::Down => self.state.down(),
Key::Left => self.state.cycle(-1),
Key::Right => self.state.cycle(1),
Key::Ctrl('s') => self.state.begin_command("w "),
Key::Char(':') => self.state.begin_command(""),
Key::Enter => return Flow::Close(true),
Key::Esc | Key::Char('q') => return Flow::Close(false),
_ => {}
}
Flow::Stay
}
}
fn close_outcome(applied: bool, state: &PanelState) -> PanelOutcome {
if applied && !state.is_noop() {
state.apply();
let persona = state.persona_action();
let model = state.chosen_model();
let personality = state.chosen_personality();
match state.saved.clone() {
Some(name) => PanelOutcome::SavedAndApplied {
name,
persona,
model,
personality,
},
None => PanelOutcome::Applied {
persona,
model,
personality,
},
}
} else {
match state.saved.clone() {
Some(name) => PanelOutcome::Saved { name },
None => PanelOutcome::Cancelled,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use newt_core::cognition::set_cli_cognition;
use newt_core::tenacity::{clear_cli_tenacity, set_cli_tenacity};
use newt_core::test_guard::GlobalSettingsGuard;
#[test]
fn panel_chrome_reads_its_colours_from_the_theme() {
use crate::theme::Role;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let rows = vec![
RowView {
label: "persona",
value: "sol".to_string(),
provenance: String::new(),
selected: true,
editable: true,
},
RowView {
label: "backend",
value: "dgx".to_string(),
provenance: String::new(),
selected: false,
editable: false,
},
];
let mut term = Terminal::new(TestBackend::new(48, 6)).unwrap();
term.draw(|f| render_panel(f, " settings ", &rows, hint_line("q quit"), 10, 12))
.unwrap();
let buf = term.backend().buffer().clone();
let corner = buf.cell((0, 0)).unwrap();
assert_eq!(
corner.symbol(),
"╭",
"the box draws, now through the SHARED chrome — rounded, like every \
other modal, which is the point of the shared edge"
);
assert_eq!(
corner.fg,
crate::theme::color(Role::ModalBorder),
"the border is themed, not left at the terminal default"
);
assert_ne!(
corner.fg,
ratatui::style::Color::Reset,
"an unstyled border is the defect this test exists for"
);
let titled = (1..47)
.map(|x| buf.cell((x, 0)).unwrap())
.find(|c| c.symbol() == "s")
.expect("the title renders on the top edge");
assert_eq!(titled.fg, crate::theme::color(Role::ModalTitle));
let row: Vec<_> = (0..48).map(|x| buf.cell((x, 1)).unwrap()).collect();
let label = row
.iter()
.find(|c| c.symbol() == "p")
.expect("the selected label renders");
assert_eq!(label.fg, crate::theme::color(Role::SelectedLabel));
let value = row
.iter()
.find(|c| c.symbol() == "‹")
.expect("the selected row carries dial chrome");
assert_eq!(value.fg, crate::theme::color(Role::SelectedValue));
}
#[test]
fn bottom_lines_read_their_colours_from_the_theme() {
use crate::theme::Role;
let ex = command_line("w");
assert_eq!(ex.spans[0].style.fg, Some(crate::theme::color(Role::Ok)));
let status = status_line("saved");
assert_eq!(
status.spans[0].style.fg,
Some(crate::theme::color(Role::Identity))
);
}
fn rendered_panel_lines(
rows: &[RowView],
bottom: Line<'static>,
width: u16,
height: u16,
) -> Vec<String> {
use ratatui::{backend::TestBackend, Terminal};
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| render_panel(frame, " panel ", rows, bottom.clone(), 10, 12))
.unwrap();
let buffer = terminal.backend().buffer();
(0..height)
.map(|y| {
(0..width)
.map(|x| buffer.cell((x, y)).unwrap().symbol())
.collect()
})
.collect()
}
fn render_test_rows(selected: Option<usize>) -> Vec<RowView> {
[
"row-00", "row-01", "row-02", "row-03", "row-04", "row-05", "row-06", "row-07",
"row-08", "row-09", "row-10",
]
.into_iter()
.enumerate()
.map(|(index, label)| RowView {
label,
value: "value".into(),
provenance: String::new(),
selected: selected == Some(index),
editable: true,
})
.collect()
}
#[test]
fn shared_panel_reveals_every_selected_row_and_keeps_the_exit_hint() {
for height in [4, 7, 14] {
for selected in 0..11 {
let rows = render_test_rows(Some(selected));
let lines = rendered_panel_lines(&rows, hint_line("Esc cancel"), 48, height);
let screen = lines.join("\n");
assert!(
lines
.iter()
.any(|line| line.contains(&format!("❯ row-{selected:02}"))),
"height={height}, selected={selected}:\n{screen}"
);
assert!(screen.contains("Esc cancel"), "{screen}");
assert_eq!(screen.matches('❯').count(), 1, "{screen}");
}
}
}
#[test]
fn shared_panel_keeps_command_and_status_footers_with_empty_or_unselected_rows() {
for rows in [
Vec::new(),
render_test_rows(None),
render_test_rows(Some(10)),
] {
for height in [3, 7] {
for (bottom, expected) in [
(command_line("w copy"), ":w copy▏"),
(status_line("save refused"), "save refused"),
] {
let screen = rendered_panel_lines(&rows, bottom, 48, height).join("\n");
assert!(screen.contains(expected), "height={height}:\n{screen}");
if height == 3 {
assert!(!screen.contains("row-"), "footer owns the only body row");
} else if !rows.is_empty() && rows.iter().all(|row| !row.selected) {
assert!(screen.contains("row-00"), "no selection starts at the top");
}
}
}
}
}
#[test]
fn shared_panel_zero_and_tiny_rectangles_are_safe() {
for width in [0, 1, 2, 48] {
for height in 0..=3 {
for rows in [Vec::new(), render_test_rows(Some(10))] {
let lines = rendered_panel_lines(&rows, hint_line("Esc cancel"), width, height);
assert_eq!(lines.len(), usize::from(height));
if width == 48 && height == 3 {
assert!(lines[1].contains("Esc cancel"), "{lines:?}");
assert!(!lines[1].contains("row-"), "footer precedes hidden rows");
}
}
}
}
}
fn choice(
name: &str,
cognition: Option<Cognition>,
initiative: Option<Initiative>,
) -> PersonaChoice {
PersonaChoice {
name: name.to_string(),
profile: Some(newt_core::RoleProfile {
prompt: format!("# {name}"),
cognition,
initiative,
backend: Some("sol".to_string()),
..Default::default()
}),
}
}
fn model_choices(names: &[&str]) -> Vec<ModelChoice> {
names
.iter()
.map(|n| ModelChoice {
name: n.to_string(),
tag: String::new(),
})
.collect()
}
fn seed(
current: Option<&str>,
personas: Vec<PersonaChoice>,
base_ini: Initiative,
models: Option<Vec<ModelChoice>>,
current_model: &str,
) -> PanelSeed {
PanelSeed {
via: "/psyche",
personas,
current_persona: current.map(str::to_string),
backend: Some("sol".to_string()),
base_initiative: base_ini,
initiative_from_family: false,
models,
current_model: current_model.to_string(),
personality: PersonalityTraits::default(),
}
}
fn panel(
current: Option<&str>,
personas: Vec<PersonaChoice>,
base_ini: Initiative,
) -> PanelState {
PanelState::new(seed(
current,
personas,
base_ini,
Some(model_choices(&["m1", "m2", "m3"])),
"m2",
))
}
fn two_personas() -> Vec<PersonaChoice> {
vec![choice("bob", None, None), choice("obsessive", None, None)]
}
fn ok_persist() -> impl FnMut(&str, &str, bool) -> SaveResult {
|name: &str, _content: &str, _overwrite: bool| SaveResult::Saved {
name: name.to_string(),
}
}
#[test]
fn untouched_dials_are_never_written_on_apply() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let s = panel(None, two_personas(), Initiative::Measured);
s.apply();
assert_eq!(
cli_tenacity(),
None,
"untouched tenacity stays un-overridden"
);
assert_eq!(cli_cognition(), CognitionOverride::Unset);
}
#[test]
fn the_dial_step_is_total_and_clamps_at_both_ends() {
assert_eq!(clamp_step(0, 1, 0), 0, "an empty list has no positions");
assert_eq!(clamp_step(0, -1, 0), 0);
assert_eq!(clamp_step(7, 1, 0), 0, "even from a stale index");
assert_eq!(clamp_step(0, 1, 1), 0);
assert_eq!(clamp_step(0, -1, 1), 0);
assert_eq!(clamp_step(0, -1, 3), 0, "the floor holds");
assert_eq!(clamp_step(2, 1, 3), 2, "the ceiling holds");
assert_eq!(clamp_step(1, 1, 3), 2);
assert_eq!(clamp_step(1, -1, 3), 0);
assert_eq!(clamp_step(9, -1, 3), 2);
assert_eq!(clamp_step(9, 1, 3), 2);
}
#[test]
fn the_panel_writes_dials_only_through_the_recorded_path() {
let source = crate::production_source(include_str!("config_panel.rs"));
for setter in [
"set_cli_cognition(",
"set_cli_tenacity(",
"clear_cli_tenacity(",
"set_cli_initiative(",
"clear_cli_initiative(",
"mark_cognition_choice(",
"mark_tenacity_choice(",
"mark_initiative_choice(",
] {
assert_eq!(
source.matches(setter).count(),
0,
"`{setter}` writes a setting with no receipt; go through \
settings_form::apply_cognition / apply_tenacity / apply_initiative"
);
}
assert!(
source.contains("settings_form::apply_cognition")
&& source.contains("settings_form::apply_tenacity")
&& source.contains("settings_form::apply_initiative"),
"the panel must apply its dials through the recording path"
);
}
#[test]
fn apply_marks_a_preference_action_only_for_the_dirty_dials() {
use newt_core::runtime::drain_preference_actions;
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let _ = drain_preference_actions();
let s = panel(None, two_personas(), Initiative::Measured);
s.apply();
assert!(
drain_preference_actions().is_empty(),
"an untouched panel must not pin"
);
let mut s = panel(None, two_personas(), Initiative::Measured);
s.down(); s.down(); s.cycle(1);
assert!(s.cognition.is_dirty() && !s.tenacity.is_dirty());
s.apply();
let a = drain_preference_actions();
assert_eq!(a.cognition, Some(s.cognition.value()));
assert_eq!(a.tenacity, None, "an untouched dial must not be pinned");
assert_eq!((a.backend, a.model), (None, None), "panel owns dials only");
let mut s = panel(None, two_personas(), Initiative::Measured);
s.down();
s.down();
s.down(); for _ in 0..ladder(Tenacity::all()).len() {
s.cycle(-1);
}
assert_eq!(s.tenacity.value(), None, "reached auto");
s.apply();
let a = drain_preference_actions();
assert_eq!(a.tenacity, Some(None), "auto unpins the axis");
assert_eq!(a.cognition, None);
assert_eq!(a.initiative, None, "an untouched initiative is not pinned");
let mut s = panel(None, two_personas(), Initiative::Measured);
for _ in 0..4 {
s.down(); }
s.cycle(1);
assert!(s.initiative.is_dirty() && !s.tenacity.is_dirty());
s.apply();
let a = drain_preference_actions();
assert_eq!(a.initiative, Some(s.initiative.value()));
assert_eq!(a.tenacity, None);
}
#[test]
fn auto_position_clears_the_tenacity_override() {
let _g = GlobalSettingsGuard::acquire();
set_cli_tenacity(Tenacity::Relentless);
let mut s = panel(None, two_personas(), Initiative::Measured);
s.down(); s.down(); s.down(); for _ in 0..ladder(Tenacity::all()).len() {
s.cycle(-1);
}
assert_eq!(s.tenacity.value(), None, "reached auto");
s.apply();
assert_eq!(cli_tenacity(), None, "auto cleared the override");
}
#[test]
fn projection_follows_the_selected_persona() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let personas = vec![
choice("bob", Some(Cognition::Rational), Some(Initiative::Measured)),
choice(
"obsessive",
Some(Cognition::Meticulous),
Some(Initiative::Eager),
),
];
let mut s = panel(Some("bob"), personas, Initiative::Patient);
assert_eq!(s.projected_cognition(), Some(Cognition::Rational));
assert_eq!(s.projected_initiative(), Initiative::Measured);
s.cycle(1); s.cycle(1); assert_eq!(s.projected_cognition(), Some(Cognition::Meticulous));
assert_eq!(s.projected_initiative(), Initiative::Eager);
let (ini_val, ini_prov) = s.initiative_cell();
assert!(ini_val.contains("eager"), "shows projected level");
assert_eq!(ini_prov, "persona: obsessive", "attributes it to obsessive");
assert_eq!(s.projected_tenacity(), Tenacity::Normal);
}
#[test]
fn projection_base_shows_when_the_persona_declares_nothing() {
let _g = GlobalSettingsGuard::acquire();
newt_core::initiative::clear_cli_initiative();
let personas = vec![choice("obsessive", None, None)];
let mut s = panel(None, personas.clone(), Initiative::Decisive);
s.cycle(1); assert_eq!(
s.projected_initiative(),
Initiative::Decisive,
"inherits base"
);
assert_eq!(s.initiative_cell().1, "base");
let mut family = seed(None, personas, Initiative::Decisive, None, "");
family.initiative_from_family = true;
let s = PanelState::new(family);
assert_eq!(
s.initiative_cell(),
("auto → decisive".to_string(), "model default".to_string())
);
}
#[test]
fn save_serializes_the_projected_posture() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let personas = vec![choice(
"bob",
Some(Cognition::Meticulous),
Some(Initiative::Eager),
)];
let mut s = panel(Some("bob"), personas, Initiative::Measured);
let mut persist = ok_persist();
s.begin_command("w clone");
assert_eq!(s.run_command(&mut persist), None);
assert_eq!(s.saved.as_deref(), Some("clone"));
let content = s.persona_content("clone").unwrap();
let rp = newt_core::RoleProfile::parse(&content).unwrap();
assert_eq!(rp.cognition, Some(Cognition::Meticulous));
assert_eq!(rp.initiative, Some(Initiative::Eager));
}
#[test]
fn save_never_bakes_in_the_model_default_initiative() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
newt_core::initiative::clear_cli_initiative();
let mut family = seed(None, two_personas(), Initiative::Eager, None, "");
family.initiative_from_family = true;
let s = PanelState::new(family);
assert_eq!(s.projected_initiative(), Initiative::Eager, "shown");
let rp = newt_core::RoleProfile::parse(&s.persona_content("fresh").unwrap()).unwrap();
assert_eq!(rp.initiative, None, "a family default is not a choice");
let mut s = panel(None, two_personas(), Initiative::Eager);
for _ in 0..4 {
s.down(); }
s.cycle(1); let rp = newt_core::RoleProfile::parse(&s.persona_content("fresh").unwrap()).unwrap();
assert_eq!(rp.initiative, Some(Initiative::Patient));
}
#[test]
fn active_persona_absent_from_the_list_is_kept_not_silently_cleared() {
let _g = GlobalSettingsGuard::acquire();
let s = panel(Some("ghost"), two_personas(), Initiative::Measured);
assert!(s.persona_label().contains("ghost"));
assert!(s.persona_label().contains("(active)"));
assert_eq!(s.persona_action(), PersonaAction::Keep);
}
#[test]
fn saving_an_unavailable_persona_refuses_instead_of_replacing_its_unknown_profile() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(Some("unavailable"), two_personas(), Initiative::Measured);
let mut writes = 0;
let mut persist = |name: &str, _content: &str, _overwrite: bool| {
writes += 1;
SaveResult::Saved { name: name.into() }
};
s.begin_command("wq! unavailable");
assert_eq!(s.run_command(&mut persist), None, "keep the draft open");
assert_eq!(writes, 0, "unknown restrictions must never be overwritten");
assert!(s.saved.is_none());
assert!(s
.status
.as_deref()
.is_some_and(|s| s.contains("unavailable")));
}
#[test]
fn saved_persona_preserves_explicit_false_and_escapes_backend_names() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let backend = "local \"quoted\" \\ route";
let personas = vec![PersonaChoice {
name: "quiet".into(),
profile: Some(newt_core::RoleProfile {
prompt: "# Quiet".into(),
backend: Some(backend.into()),
crew: Some(false),
..Default::default()
}),
}];
let s = panel(Some("quiet"), personas, Initiative::Measured);
let saved = newt_core::RoleProfile::parse(&s.persona_content("quiet-copy").unwrap())
.expect("the panel must serialize valid TOML, including quoted values");
assert_eq!(saved.backend.as_deref(), Some(backend));
assert_eq!(
saved.crew,
Some(false),
"explicit false must not become inherit"
);
}
#[test]
#[serial_test::serial(real_fs)]
fn saving_invalid_metadata_never_overwrites_an_existing_persona() {
let _guard = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let original = "+++\ntools = [\"read_file\"]\n[caveats]\nfs_write = \"none\"\nexec = \"none\"\n+++\n\nKeep these restrictions and this prose.\n";
for (case, backend) in [
("multiline fence", "first\n+++\nlast".to_string()),
(
"oversized header",
"x".repeat(newt_core::markup::MAX_ENVELOPE_BYTES + 1),
),
] {
let tmp = tempfile::tempdir().unwrap();
let store = crate::PersonaStore::new(tmp.path());
let path = store.save("protected", original, false).unwrap();
let before = store.load("protected").unwrap();
let mut state = panel(
Some("candidate"),
vec![PersonaChoice {
name: "candidate".into(),
profile: Some(newt_core::RoleProfile {
prompt: "Candidate body remains in the draft.".into(),
backend: Some(backend),
..Default::default()
}),
}],
Initiative::Measured,
);
let mut writes = 0;
let mut persist = |name: &str, content: &str, overwrite: bool| {
writes += 1;
match store.save(name, content, overwrite) {
Ok(_) => SaveResult::Saved { name: name.into() },
Err(err) => SaveResult::Failed(format!("{err:?}")),
}
};
state.begin_command("wq! protected");
let outcome = state.run_command(&mut persist);
assert_eq!(std::fs::read_to_string(&path).unwrap(), original, "{case}");
assert_eq!(
writes, 0,
"invalid metadata must not reach persistence: {case}"
);
assert_eq!(outcome, None, "failed save keeps the draft open: {case}");
assert!(state.saved.is_none(), "{case}");
assert!(
state
.status
.as_deref()
.is_some_and(|s| s.contains("save failed")),
"{case}: {:?}",
state.status
);
assert_eq!(store.load("protected").unwrap(), before, "{case}");
assert_eq!(state.persona_action(), PersonaAction::Keep, "{case}");
}
}
#[test]
#[serial_test::serial(real_fs)]
fn saving_a_loaded_persona_preserves_its_full_restrictive_profile() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let tmp = tempfile::tempdir().unwrap();
let store = crate::PersonaStore::new(tmp.path());
let document = r#"+++
role = "reviewer"
tools = ["read_file"]
skills = ["code-review"]
model = "review-model"
tier = "REVIEW"
altitude = "coach"
backend = "review-backend"
cognition = "rational"
tenacity = "relaxed"
crew = false
[caveats]
fs_read = ["src/"]
fs_write = "none"
exec = "none"
net = "none"
max_calls = 3
+++
# Restricted reviewer
Read the code and explain findings. Do not edit files or run commands.
"#;
store.save("restricted", document, false).unwrap();
let original = store.load("restricted").unwrap();
let mut s = panel(
Some("restricted"),
vec![PersonaChoice::from(original.clone())],
Initiative::Measured,
);
let mut persist = |name: &str, content: &str, overwrite: bool| match store
.save(name, content, overwrite)
{
Ok(_) => SaveResult::Saved { name: name.into() },
Err(err) => SaveResult::Failed(format!("{err:?}")),
};
s.begin_command("w restricted-copy");
assert_eq!(
s.run_command(&mut persist),
None,
"save alone does not apply"
);
assert_eq!(s.saved.as_deref(), Some("restricted-copy"));
let saved = store.load("restricted-copy").unwrap();
assert_eq!(saved.profile, original.profile);
assert_eq!(saved.prompt, original.prompt);
assert_eq!(store.load("restricted").unwrap(), original);
}
#[test]
#[serial_test::serial(real_fs)]
fn style_only_draft_save_snapshots_projected_posture_and_preserves_restrictions() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Set(Cognition::Meticulous));
set_cli_tenacity(Tenacity::Relentless);
newt_core::initiative::set_cli_initiative(Initiative::Decisive);
newt_core::process_env::set_var("NEWT_TEAM", "1");
let _ = newt_core::runtime::drain_preference_actions();
let tmp = tempfile::tempdir().unwrap();
let store = crate::PersonaStore::new(tmp.path());
let document = "+++\nrole = \"reviewer\"\ntools = [\"read_file\"]\n\
skills = [\"code-review\"]\nmodel = \"review-model\"\ntier = \"REVIEW\"\n\
altitude = \"coach\"\n[caveats]\nfs_read = [\"src/\"]\n\
fs_write = \"none\"\nexec = \"none\"\nnet = \"none\"\nmax_calls = 3\n\
+++\n\n# Sparse reviewer\n\nRead the code and explain findings without editing.\n";
let original_path = store.save("sparse", document, false).unwrap();
let original = store.load("sparse").unwrap();
assert!(original.profile.backend.is_none() && original.profile.crew.is_none());
assert!(original.profile.cognition.is_none() && original.profile.initiative.is_none());
let mut s = panel(
Some("sparse"),
vec![PersonaChoice::from(original.clone())],
Initiative::Measured,
);
for _ in 0..7 {
s.down(); }
s.cycle(1); assert_eq!(s.persona_action(), PersonaAction::Keep);
assert!(!s.cognition.is_dirty() && !s.tenacity.is_dirty() && !s.model.is_dirty());
let mut persist = |name: &str, content: &str, overwrite: bool| match store
.save(name, content, overwrite)
{
Ok(_) => SaveResult::Saved { name: name.into() },
Err(err) => SaveResult::Failed(format!("{err:?}")),
};
s.begin_command("w sparse-copy");
assert_eq!(
s.run_command(&mut persist),
None,
"save alone does not apply"
);
assert_eq!(s.saved.as_deref(), Some("sparse-copy"));
let mut expected = original.profile.clone();
expected.backend = Some("sol".into()); expected.cognition = Some(Cognition::Meticulous);
expected.initiative = Some(Initiative::Decisive);
expected.crew = Some(true);
expected.personality = Some(PersonalityTraits {
warmth: Some(PersonalityLevel::try_from(0).unwrap()),
..Default::default()
});
let saved = store.load("sparse-copy").unwrap();
assert_eq!(
saved.profile, expected,
"only projected panel fields change"
);
assert_eq!(saved.prompt, original.prompt);
assert_eq!(std::fs::read_to_string(original_path).unwrap(), document);
assert_eq!(store.load("sparse").unwrap(), original);
assert_eq!(
cli_cognition(),
CognitionOverride::Set(Cognition::Meticulous)
);
assert_eq!(cli_tenacity(), Some(Tenacity::Relentless));
assert!(newt_core::runtime::drain_preference_actions().is_empty());
}
#[test]
fn personality_controls_are_independent_named_rows() {
let _g = GlobalSettingsGuard::acquire();
let s = panel(None, two_personas(), Initiative::Measured);
let rows = s.view_rows();
for label in [
"agreeableness",
"extraversion",
"warmth",
"approachability",
"prosocial behavior",
] {
let matches: Vec<_> = rows.iter().filter(|row| row.label == label).collect();
assert_eq!(matches.len(), 1, "one operator dial for {label}");
assert!(matches[0].editable, "{label} is an operator control");
}
}
#[test]
fn personality_panel_keeps_the_selected_dial_and_exit_hint_visible_when_short() {
use ratatui::{backend::TestBackend, Terminal};
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(None, two_personas(), Initiative::Measured);
for _ in 0..9 {
s.down();
}
let mut term = Terminal::new(TestBackend::new(100, 7)).unwrap();
term.draw(|f| draw(f, &s)).unwrap();
let buf = term.backend().buffer();
let screen: String = buf.content.iter().map(|cell| cell.symbol()).collect();
assert!(screen.contains("❯ prosocial behavior"), "{screen}");
assert!(screen.contains("Esc cancel"), "{screen}");
}
#[test]
fn personality_apply_returns_only_edited_axes_after_an_explicit_apply() {
use newt_core::role_profile::{PersonalityLevel, PersonalityTrait, PersonalityTraits};
let _g = GlobalSettingsGuard::acquire();
let mut initial = seed(None, two_personas(), Initiative::Measured, None, "");
initial.personality = PersonalityTraits {
warmth: Some(PersonalityLevel::try_from(73).unwrap()),
..Default::default()
};
let mut s = PanelState::new(initial);
assert_eq!(close_outcome(true, &s), PanelOutcome::Cancelled);
for _ in 0..5 {
s.down();
}
s.cycle(1);
assert_eq!(close_outcome(false, &s), PanelOutcome::Cancelled);
assert_eq!(
close_outcome(true, &s),
PanelOutcome::Applied {
persona: PersonaAction::Keep,
model: None,
personality: vec![(
PersonalityTrait::Agreeableness,
Some(PersonalityLevel::try_from(0).unwrap()),
)],
}
);
}
#[test]
fn personality_clear_does_not_reapply_untouched_overrides_to_the_new_conversation() {
use newt_core::role_profile::{PersonalityLevel, PersonalityTrait, PersonalityTraits};
let _g = GlobalSettingsGuard::acquire();
let mut initial = seed(Some("bob"), two_personas(), Initiative::Measured, None, "");
initial.personality = PersonalityTraits {
warmth: Some(PersonalityLevel::try_from(73).unwrap()),
..Default::default()
};
let mut s = PanelState::new(initial);
s.cycle(-1); for _ in 0..5 {
s.down();
}
s.cycle(1);
assert_eq!(
close_outcome(true, &s),
PanelOutcome::Applied {
persona: PersonaAction::Clear,
model: None,
personality: vec![(
PersonalityTrait::Agreeableness,
Some(PersonalityLevel::try_from(0).unwrap()),
)],
}
);
let saved = newt_core::RoleProfile::parse(&s.persona_content("copy").unwrap()).unwrap();
assert_eq!(
saved.personality.unwrap().warmth,
None,
"the saved preview agrees with the new-conversation reset"
);
}
#[test]
fn personality_dials_save_each_axis_without_switching_persona_or_other_dials() {
use newt_core::role_profile::{PersonalityLevel, PersonalityTrait};
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
for (index, kind) in PersonalityTrait::ALL.into_iter().enumerate() {
let mut s = panel(
Some("base"),
vec![choice("base", None, None)],
Initiative::Measured,
);
for _ in 0..5 + index {
s.down();
}
s.cycle(1); assert_eq!(s.persona_action(), PersonaAction::Keep, "{}", kind.key());
assert!(!s.cognition.is_dirty() && !s.tenacity.is_dirty());
assert!(!s.is_noop(), "moving {} is an explicit edit", kind.key());
let saved = newt_core::RoleProfile::parse(&s.persona_content("copy").unwrap()).unwrap();
let traits = saved.personality.expect("save the projected style");
for other in PersonalityTrait::ALL {
let expected = (other == kind).then(|| PersonalityLevel::try_from(0).unwrap());
assert_eq!(
traits.get(other),
expected,
"{} / {}",
kind.key(),
other.key()
);
}
assert_eq!(close_outcome(false, &s), PanelOutcome::Cancelled);
}
}
#[test]
fn personality_dial_projects_persona_inheritance_and_validated_endpoints() {
use newt_core::role_profile::{PersonalityLevel, PersonalityTrait};
let _g = GlobalSettingsGuard::acquire();
let profile = newt_core::RoleProfile::parse(
"+++\n[personality]\nagreeableness = 73\n+++\nKeep the review evidence-based.",
)
.unwrap();
let mut s = panel(
Some("reviewer"),
vec![PersonaChoice {
name: "reviewer".into(),
profile: Some(profile.clone()),
}],
Initiative::Measured,
);
let saved = |s: &PanelState| {
newt_core::RoleProfile::parse(&s.persona_content("copy").unwrap()).unwrap()
};
assert_eq!(
saved(&s).personality,
profile.personality,
"untouched inherits"
);
for _ in 0..5 {
s.down();
}
s.cycle(1);
assert_eq!(
saved(&s)
.personality
.unwrap()
.get(PersonalityTrait::Agreeableness),
Some(PersonalityLevel::try_from(0).unwrap())
);
for _ in 0..110 {
s.cycle(1);
}
assert_eq!(
saved(&s)
.personality
.unwrap()
.get(PersonalityTrait::Agreeableness),
Some(PersonalityLevel::try_from(100).unwrap())
);
for _ in 0..110 {
s.cycle(-1);
}
assert_eq!(
saved(&s).personality,
profile.personality,
"inherit releases the override"
);
assert_eq!(
saved(&s).prompt,
profile.prompt,
"style edits retain the body"
);
}
#[test]
fn a_noop_visit_is_detected_so_enter_closes_silently() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(Some("bob"), two_personas(), Initiative::Measured);
assert!(s.is_noop(), "fresh panel: nothing to apply");
s.down();
s.up();
assert!(s.is_noop(), "row selection alone is not a change");
s.down(); s.down(); s.cycle(1);
assert!(!s.is_noop(), "a touched dial is a change");
s.cycle(-1);
assert!(!s.is_noop(), "cycled back is still dirty (re-apply)");
let mut p = panel(Some("bob"), two_personas(), Initiative::Measured);
while p.persona_idx != 0 {
p.cycle(-1);
}
assert!(!p.is_noop(), "persona Clear is a change");
let mut m = panel(Some("bob"), two_personas(), Initiative::Measured);
m.down(); m.cycle(1);
assert!(!m.is_noop(), "a touched model spinner is a change");
}
#[test]
fn model_spinner_dials_served_models_and_reports_only_a_touched_pick() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(None, two_personas(), Initiative::Measured);
assert_eq!(s.chosen_model(), None, "untouched spinner picks nothing");
assert!(
s.model_label().contains("m2") && s.model_label().contains("(active)"),
"opens on the active model: {}",
s.model_label()
);
s.down(); s.cycle(1); assert_eq!(s.chosen_model().as_deref(), Some("m3"));
assert_eq!(
close_outcome(true, &s),
PanelOutcome::Applied {
persona: PersonaAction::Keep,
model: Some("m3".to_string()),
personality: vec![],
}
);
assert!(
s.model_label().contains("(pending)"),
"a non-active position renders pending: {}",
s.model_label()
);
s.cycle(-1);
assert_eq!(s.chosen_model().as_deref(), Some("m2"));
s.cycle(-1);
s.cycle(-1);
assert_eq!(s.chosen_model().as_deref(), Some("m1"));
}
#[test]
fn an_active_model_missing_from_the_served_list_is_appended_not_lost() {
let _g = GlobalSettingsGuard::acquire();
let s = PanelState::new(seed(
None,
two_personas(),
Initiative::Measured,
Some(model_choices(&["m1", "m2"])),
"ghost-model",
));
assert!(
s.model_label().contains("ghost-model")
&& s.model_label().contains("(not served)")
&& s.model_label().contains("(active)"),
"{}",
s.model_label()
);
assert_eq!(s.chosen_model(), None, "still untouched");
}
#[test]
fn an_unreachable_backend_disables_the_model_dial() {
let _g = GlobalSettingsGuard::acquire();
let mut s = PanelState::new(seed(None, two_personas(), Initiative::Measured, None, "m2"));
assert!(
s.model_label().contains("unreachable"),
"{}",
s.model_label()
);
s.down(); s.cycle(1);
assert_eq!(s.chosen_model(), None, "cycling a dead row picks nothing");
assert!(s.is_noop(), "a dead model row cannot dirty the visit");
}
#[test]
fn no_backend_persona_projects_the_operator_baseline() {
let _g = GlobalSettingsGuard::acquire();
let personas = vec![PersonaChoice {
name: "alice".to_string(),
profile: Some(newt_core::RoleProfile {
prompt: "# Alice".into(),
..Default::default()
}),
}];
let mut s = PanelState::new(seed(None, personas, Initiative::Measured, None, ""));
s.cycle(1); assert_eq!(
s.projected_backend().as_deref(),
Some("sol"),
"no-backend persona reverts to the baseline"
);
assert_eq!(s.backend_provenance(), "base");
}
#[test]
fn persona_row_shows_active_and_maps_none_to_clear() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(Some("bob"), two_personas(), Initiative::Measured);
assert_eq!(
s.persona_action(),
PersonaAction::Keep,
"opens on the active"
);
assert!(s.persona_label().contains("(active)"));
while s.persona_idx != 0 {
s.cycle(-1);
}
assert_eq!(s.persona_action(), PersonaAction::Clear);
assert!(s.persona_label().contains("clears active"));
s.cycle(1); s.cycle(1); assert_eq!(
s.persona_action(),
PersonaAction::Switch("obsessive".to_string())
);
assert!(s.persona_label().contains("(pending)"));
}
#[test]
fn wq_success_saves_then_signals_apply() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(None, two_personas(), Initiative::Measured);
let mut persist = ok_persist();
s.begin_command("wq alice");
assert_eq!(
s.run_command(&mut persist),
Some(true),
":wq closes with apply"
);
assert_eq!(s.saved.as_deref(), Some("alice"));
}
#[test]
fn wq_failed_save_does_not_apply_and_stays_open() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(None, two_personas(), Initiative::Measured);
s.down(); s.down(); s.cycle(1); assert!(s.cognition.is_dirty());
let mut persist =
|_n: &str, _c: &str, _o: bool| SaveResult::Failed("disk full".to_string());
s.begin_command("wq alice");
assert_eq!(
s.run_command(&mut persist),
None,
"failed :wq does NOT close (so the caller never applies)"
);
assert!(s.saved.is_none(), "nothing recorded as saved");
assert!(
s.status.as_deref().unwrap().contains("disk full"),
"the failure is visible"
);
assert!(s.cognition.is_dirty(), "edits intact for a retry");
}
#[test]
fn wq_exists_without_bang_is_refused_visibly() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(None, two_personas(), Initiative::Measured);
let mut persist = |name: &str, _c: &str, overwrite: bool| {
if overwrite {
SaveResult::Saved {
name: name.to_string(),
}
} else {
SaveResult::Exists {
name: name.to_string(),
}
}
};
s.begin_command("wq bob");
assert_eq!(s.run_command(&mut persist), None, "exists → stay open");
assert!(s.status.as_deref().unwrap().contains("exists"));
assert!(s.saved.is_none());
s.begin_command("wq! bob");
assert_eq!(s.run_command(&mut persist), Some(true));
assert_eq!(s.saved.as_deref(), Some("bob"));
}
#[test]
fn ex_commands_validate_visibly() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(None, two_personas(), Initiative::Measured);
let mut persist = ok_persist();
s.begin_command("wq");
assert_eq!(
s.run_command(&mut persist),
None,
":wq without a name does not close"
);
assert!(s.status.as_deref().unwrap().contains("needs a name"));
assert!(s.saved.is_none());
s.begin_command("banana");
assert_eq!(s.run_command(&mut persist), None);
assert!(s.status.as_deref().unwrap().contains("unknown command"));
s.begin_command("q");
assert_eq!(s.run_command(&mut persist), Some(false));
}
#[test]
fn w_then_cancel_is_saved_not_applied() {
let _g = GlobalSettingsGuard::acquire();
let mut s = panel(None, two_personas(), Initiative::Measured);
let mut persist = ok_persist();
s.begin_command("w keep");
s.run_command(&mut persist);
assert_eq!(s.saved.as_deref(), Some("keep"));
assert_eq!(
close_outcome(false, &s),
PanelOutcome::Saved {
name: "keep".to_string()
}
);
assert_eq!(
close_outcome(true, &s),
PanelOutcome::Saved {
name: "keep".to_string()
}
);
}
#[test]
fn close_outcome_downgrades_a_noop_enter_and_applies_a_real_one() {
use newt_core::cognition::{set_cli_cognition, CognitionOverride};
use newt_core::initiative::{cli_initiative, set_cli_initiative};
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
set_cli_tenacity(Tenacity::Normal);
set_cli_initiative(Initiative::Measured);
let s = panel(None, two_personas(), Initiative::Measured);
assert_eq!(close_outcome(true, &s), PanelOutcome::Cancelled);
assert_eq!(close_outcome(false, &s), PanelOutcome::Cancelled);
assert_eq!(cli_tenacity(), Some(Tenacity::Normal), "nothing applied");
let applied = PanelOutcome::Applied {
persona: PersonaAction::Keep,
model: None,
personality: vec![],
};
let mut t = panel(None, two_personas(), Initiative::Measured);
t.down(); t.down(); t.down(); t.cycle(1); assert_eq!(close_outcome(true, &t), applied);
assert_eq!(
cli_tenacity(),
Some(Tenacity::Relentless),
"the tenacity-only edit was applied, not silently discarded"
);
set_cli_tenacity(Tenacity::Normal);
assert_eq!(close_outcome(false, &t), PanelOutcome::Cancelled);
assert_eq!(cli_tenacity(), Some(Tenacity::Normal));
let mut i = panel(None, two_personas(), Initiative::Measured);
for _ in 0..4 {
i.down(); }
i.cycle(1); assert_eq!(close_outcome(true, &i), applied);
assert_eq!(cli_initiative(), Some(Initiative::Decisive));
}
#[test]
fn panel_choices_use_the_loaded_active_profile_instead_of_changed_disk_content() {
let mut active = crate::test_persona("reviewer", "The loaded prompt.", "loaded.md".into());
active.profile.tools = Some(vec!["read_file".into()]);
active.profile.crew = Some(false);
let disk = crate::test_persona("reviewer", "A later disk edit.", "edited.md".into());
let other = crate::test_persona("writer", "Another persona.", "writer.md".into());
let choices = PersonaChoice::for_panel(vec![disk, other.clone()], Some(&active));
assert_eq!(choices.len(), 2, "the active name must not be duplicated");
assert_eq!(
choices.iter().find(|p| p.name == "reviewer"),
Some(&PersonaChoice::from(active)),
"Keep previews the profile already loaded by the chat loop"
);
assert_eq!(
choices.iter().find(|p| p.name == "writer"),
Some(&PersonaChoice::from(other)),
"unselected disk candidates remain unchanged"
);
}
#[test]
#[serial_test::serial(real_fs)]
fn panel_choices_keep_a_missing_but_loaded_active_persona_saveable() {
let _g = GlobalSettingsGuard::acquire();
set_cli_cognition(CognitionOverride::Unset);
clear_cli_tenacity();
let tmp = tempfile::tempdir().unwrap();
let store = crate::PersonaStore::new(tmp.path());
store
.save(
"reviewer",
r#"+++
role = "reviewer"
tools = ["read_file"]
backend = "sol"
tenacity = "standard"
crew = false
[caveats]
fs_write = "none"
exec = "none"
net = "none"
[personality]
warmth = 73
+++
Keep this loaded prompt and its restrictions.
"#,
false,
)
.unwrap();
let active = store.load("reviewer").unwrap();
std::fs::remove_file(&active.path).unwrap();
assert!(store.load("reviewer").is_err(), "the source is absent");
let choices = PersonaChoice::for_panel(Vec::new(), Some(&active));
assert_eq!(choices, vec![PersonaChoice::from(active.clone())]);
let mut s = panel(Some("reviewer"), choices, Initiative::Measured);
let mut persist = |name: &str, content: &str, overwrite: bool| match store
.save(name, content, overwrite)
{
Ok(_) => SaveResult::Saved { name: name.into() },
Err(err) => SaveResult::Failed(format!("{err:?}")),
};
s.begin_command("w reviewer-copy");
assert_eq!(s.run_command(&mut persist), None, "save is not Apply");
assert_eq!(s.saved.as_deref(), Some("reviewer-copy"));
let saved = store.load("reviewer-copy").unwrap();
assert_eq!(saved.profile, active.profile);
assert_eq!(saved.prompt, active.prompt);
}
#[test]
fn panel_choices_without_an_active_persona_preserve_all_loaded_candidates() {
let mut first = crate::test_persona("first", "First prompt.", "first.md".into());
first.profile.crew = Some(false);
let mut second = crate::test_persona("second", "Second prompt.", "second.md".into());
second.profile.tools = Some(vec!["read_file".into()]);
let expected = vec![
PersonaChoice::from(first.clone()),
PersonaChoice::from(second.clone()),
];
assert_eq!(
PersonaChoice::for_panel(vec![first, second], None),
expected
);
}
}