alpha-readiness: Agent Health screen, $MEMORY_DIR fix, tool posture badge
- Settings Agent category now shows agent ID, subconscious pairing, memory/subconscious paths, and disk-existence checks (read-only) - $MEMORY_DIR env var now always set correctly — strips stale host-shell leftovers before injecting agent-specific computed values - Header bar shows [tools shown] / [tools folded] posture badge - Fix "applies live" footer — only shown for categories with live fields - Bifrost "primary model" relabeled to "new-agent default" for clarity - Update alpha-tester-readiness.md task doc and INDEX.md task tracker
This commit is contained in:
parent
4194f00635
commit
74887d32a3
6 changed files with 116 additions and 36 deletions
|
|
@ -113,8 +113,9 @@ impl ToolContext {
|
|||
/// - `AGENT_ID` / `LETTA_AGENT_ID` / `SOUVERAINE_AGENT_ID` — her own
|
||||
/// identifier so skills that scope by agent can resolve.
|
||||
///
|
||||
/// Any value already present in `env` is left alone — the caller's
|
||||
/// override wins.
|
||||
/// Memory and agent-id vars are always set (overriding any stale values
|
||||
/// inherited from the host shell — e.g. a leftover `$MEMORY_DIR` from
|
||||
/// the Letta era). Other env keys from the caller are preserved.
|
||||
pub fn for_agent(
|
||||
agent_id: impl Into<String>,
|
||||
cwd: Option<PathBuf>,
|
||||
|
|
@ -124,18 +125,22 @@ impl ToolContext {
|
|||
) -> Self {
|
||||
let agent_id_str = agent_id.into();
|
||||
|
||||
// Strip stale values so the computed override always wins.
|
||||
env.retain(|(k, _)| {
|
||||
!matches!(k.as_str(),
|
||||
"MEMORY_DIR" | "LETTA_MEMORY_DIR" | "SOUVERAINE_MEMORY_DIR" | "MEMORY"
|
||||
| "AGENT_ID" | "LETTA_AGENT_ID" | "SOUVERAINE_AGENT_ID"
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(root) = memory_root.as_ref() {
|
||||
let root_str = root.display().to_string();
|
||||
for key in ["MEMORY_DIR", "LETTA_MEMORY_DIR", "SOUVERAINE_MEMORY_DIR", "MEMORY"] {
|
||||
if !env.iter().any(|(k, _)| k == key) {
|
||||
env.push((key.to_string(), root_str.clone()));
|
||||
}
|
||||
env.push((key.to_string(), root_str.clone()));
|
||||
}
|
||||
}
|
||||
for key in ["AGENT_ID", "LETTA_AGENT_ID", "SOUVERAINE_AGENT_ID"] {
|
||||
if !env.iter().any(|(k, _)| k == key) {
|
||||
env.push((key.to_string(), agent_id_str.clone()));
|
||||
}
|
||||
env.push((key.to_string(), agent_id_str.clone()));
|
||||
}
|
||||
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -233,11 +233,21 @@ impl App {
|
|||
self.chat.as_ref()?; // a backend is required to persist the change
|
||||
let id = self.agent_id_by_name(&self.agent_pref)?;
|
||||
let model = Self::agent_model_from_disk(&id)?;
|
||||
let base = dirs::home_dir()?;
|
||||
let memory_root = base.join(".souveraine/agents").join(&id).join("memory");
|
||||
let subconscious_root = base.join(".souveraine/subconscious-agents").join(format!("{id}-sub")).join("memory.git");
|
||||
let has_subconscious = subconscious_root.join("HEAD").exists();
|
||||
let agent_json_exists = base.join(".souveraine/server/agents").join(&id).join("agent.json").exists();
|
||||
Some(crate::ui::settings::ActiveAgentSettings {
|
||||
id,
|
||||
id: id.clone(),
|
||||
name: self.agent_pref.clone(),
|
||||
model: model.clone(),
|
||||
model_original: model,
|
||||
subconscious_id: format!("{id}-sub"),
|
||||
memory_root,
|
||||
subconscious_root,
|
||||
has_subconscious,
|
||||
agent_json_exists,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,10 +152,19 @@ fn draw_header(f: &mut Frame, state: &ChatState, area: Rect) {
|
|||
"remote" => state.palette.agent_primary,
|
||||
_ => state.palette.agent_dim,
|
||||
};
|
||||
let posture_label = match state.render_mode {
|
||||
super::ChatMode::Code => "tools shown",
|
||||
super::ChatMode::Conversation => "tools folded",
|
||||
};
|
||||
let posture_color = match state.render_mode {
|
||||
super::ChatMode::Code => state.palette.tool_accent,
|
||||
super::ChatMode::Conversation => state.palette.agent_dim,
|
||||
};
|
||||
let title = Line::from(vec![
|
||||
Span::styled("✦ Souveraine ", Style::default().fg(state.palette.agent_primary).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(format!("· {} ", state.agent_name), Style::default().fg(Color::White)),
|
||||
Span::styled(format!("[{} mode]", state.mode), Style::default().fg(mode_color)),
|
||||
Span::styled(format!("[{}] ", state.mode), Style::default().fg(mode_color)),
|
||||
Span::styled(format!("[{}]", posture_label), Style::default().fg(posture_color)),
|
||||
]);
|
||||
f.render_widget(Paragraph::new(title).alignment(Alignment::Center), area);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,11 @@ fn draw_field_panel(frame: &mut Frame, area: Rect, view: &SettingsView) {
|
|||
lines.push(Line::from(""));
|
||||
|
||||
let p = &view.palette;
|
||||
let has_applies_live = fields.iter().any(|(loc, _)| loc.applies_live());
|
||||
|
||||
for (i, (loc, value)) in fields.iter().enumerate() {
|
||||
let is_selected = i == view.field_idx && view.focus == PanelFocus::Fields;
|
||||
let is_readonly = loc.is_readonly();
|
||||
let is_selected = i == view.field_idx && view.focus == PanelFocus::Fields && !is_readonly;
|
||||
let key_style = if is_selected {
|
||||
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
|
|
@ -103,24 +106,26 @@ fn draw_field_panel(frame: &mut Frame, area: Rect, view: &SettingsView) {
|
|||
];
|
||||
|
||||
// If this field is being edited, show the buffer with cursor.
|
||||
if let SettingsMode::Editing { loc: edit_loc, buffer, cursor } = &view.mode {
|
||||
if *edit_loc == *loc {
|
||||
let before = &buffer[..*cursor];
|
||||
let after = &buffer[*cursor..];
|
||||
spans.push(Span::styled(
|
||||
before.to_string(),
|
||||
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
'▏'.to_string(),
|
||||
Style::default().fg(Color::White).add_modifier(Modifier::SLOW_BLINK),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
after.to_string(),
|
||||
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
lines.push(Line::from(spans));
|
||||
continue;
|
||||
if !is_readonly {
|
||||
if let SettingsMode::Editing { loc: edit_loc, buffer, cursor } = &view.mode {
|
||||
if *edit_loc == *loc {
|
||||
let before = &buffer[..*cursor];
|
||||
let after = &buffer[*cursor..];
|
||||
spans.push(Span::styled(
|
||||
before.to_string(),
|
||||
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
'▏'.to_string(),
|
||||
Style::default().fg(Color::White).add_modifier(Modifier::SLOW_BLINK),
|
||||
));
|
||||
spans.push(Span::styled(
|
||||
after.to_string(),
|
||||
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
lines.push(Line::from(spans));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,11 +136,13 @@ fn draw_field_panel(frame: &mut Frame, area: Rect, view: &SettingsView) {
|
|||
lines.push(Line::from(spans));
|
||||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" ◆ applies live · other changes take effect on restart",
|
||||
Style::default().fg(p.agent_dim).add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
if has_applies_live {
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" ◆ applies live · other changes take effect on restart",
|
||||
Style::default().fg(p.agent_dim).add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
}
|
||||
|
||||
let focused = view.focus == PanelFocus::Fields;
|
||||
let border_color = if view.dirty {
|
||||
|
|
|
|||
|
|
@ -173,6 +173,12 @@ pub enum FieldLoc {
|
|||
// TUI
|
||||
TuShowInterstitial,
|
||||
TuCennoThreshold,
|
||||
// Agent (read-only diagnostics)
|
||||
AgAgentId,
|
||||
AgSubconsciousId,
|
||||
AgMemoryPath,
|
||||
AgSubconsciousPath,
|
||||
AgSubconsciousStatus,
|
||||
}
|
||||
|
||||
impl FieldLoc {
|
||||
|
|
@ -198,6 +204,8 @@ impl FieldLoc {
|
|||
FieldLoc::PrPulseEnabled | FieldLoc::PrPulseIntervalSecs | FieldLoc::PrOutfit | FieldLoc::PrAtmosphere => Category::Presence,
|
||||
FieldLoc::VcEnabled | FieldLoc::VcSttUrl | FieldLoc::VcTtsUrl | FieldLoc::VcVoiceId | FieldLoc::VcPushToTalkKey => Category::Voice,
|
||||
FieldLoc::TuShowInterstitial | FieldLoc::TuCennoThreshold => Category::Tui,
|
||||
FieldLoc::AgAgentId | FieldLoc::AgSubconsciousId | FieldLoc::AgMemoryPath
|
||||
| FieldLoc::AgSubconsciousPath | FieldLoc::AgSubconsciousStatus => Category::Agent,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -272,6 +280,11 @@ impl FieldLoc {
|
|||
FieldLoc::FdAutoWake => "auto_wake",
|
||||
FieldLoc::TuShowInterstitial => "show_interstitial",
|
||||
FieldLoc::TuCennoThreshold => "cenno_word_threshold",
|
||||
FieldLoc::AgAgentId => "agent_id",
|
||||
FieldLoc::AgSubconsciousId => "subconscious_id",
|
||||
FieldLoc::AgMemoryPath => "memory_path",
|
||||
FieldLoc::AgSubconsciousPath => "subconscious_path",
|
||||
FieldLoc::AgSubconsciousStatus => "subconscious_ok",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +293,18 @@ impl FieldLoc {
|
|||
matches!(self, FieldLoc::BfApiKey | FieldLoc::BfVirtualKey)
|
||||
}
|
||||
|
||||
/// Returns true for informational fields that cannot be edited.
|
||||
pub fn is_readonly(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
FieldLoc::AgAgentId
|
||||
| FieldLoc::AgSubconsciousId
|
||||
| FieldLoc::AgMemoryPath
|
||||
| FieldLoc::AgSubconsciousPath
|
||||
| FieldLoc::AgSubconsciousStatus
|
||||
)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
FieldLoc::AgSystemPrompt => "platform prompt",
|
||||
|
|
@ -287,7 +312,7 @@ impl FieldLoc {
|
|||
FieldLoc::BfBaseUrl => "endpoint",
|
||||
FieldLoc::BfApiKey => "API key",
|
||||
FieldLoc::BfVirtualKey => "virtual key",
|
||||
FieldLoc::BfPrimaryModel => "primary model",
|
||||
FieldLoc::BfPrimaryModel => "new-agent default",
|
||||
FieldLoc::ScN1Enabled => "N+1 enabled",
|
||||
FieldLoc::ScN1Trigger => "N+1 trigger",
|
||||
FieldLoc::ScN1Every => " └ every N responses",
|
||||
|
|
@ -351,6 +376,11 @@ impl FieldLoc {
|
|||
FieldLoc::FdAutoWake => "auto-wake on summon",
|
||||
FieldLoc::TuShowInterstitial => "interstitial narration",
|
||||
FieldLoc::TuCennoThreshold => "cenno threshold (words)",
|
||||
FieldLoc::AgAgentId => "agent id",
|
||||
FieldLoc::AgSubconsciousId => "subconscious",
|
||||
FieldLoc::AgMemoryPath => "memory path",
|
||||
FieldLoc::AgSubconsciousPath => "subconscious path",
|
||||
FieldLoc::AgSubconsciousStatus => "subconscious on disk",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
|
|
@ -24,6 +24,16 @@ pub struct ActiveAgentSettings {
|
|||
pub model: String,
|
||||
/// The model as loaded — for save-time diffing.
|
||||
pub model_original: String,
|
||||
/// Display-only: the paired subconscious agent id (always `{id}-sub`).
|
||||
pub subconscious_id: String,
|
||||
/// Display-only: the primary's memory directory path.
|
||||
pub memory_root: PathBuf,
|
||||
/// Display-only: the subconscious agent's memory directory path.
|
||||
pub subconscious_root: PathBuf,
|
||||
/// Display-only: whether the subconscious directory + memory.git exist.
|
||||
pub has_subconscious: bool,
|
||||
/// Display-only: whether the agent's own agent.json is readable.
|
||||
pub agent_json_exists: bool,
|
||||
}
|
||||
|
||||
pub struct SettingsView {
|
||||
|
|
@ -154,6 +164,12 @@ impl SettingsView {
|
|||
let idx = variants.iter().position(|m| m == &agent.model).unwrap_or(0);
|
||||
out.push((AgModel, EditableValue::EnumVariant { index: idx, variants }));
|
||||
}
|
||||
// Read-only diagnostics
|
||||
out.push((AgAgentId, EditableValue::Text(agent.id.clone())));
|
||||
out.push((AgSubconsciousId, EditableValue::Text(agent.subconscious_id.clone())));
|
||||
out.push((AgMemoryPath, EditableValue::Text(agent.memory_root.to_string_lossy().to_string())));
|
||||
out.push((AgSubconsciousPath, EditableValue::Text(agent.subconscious_root.to_string_lossy().to_string())));
|
||||
out.push((AgSubconsciousStatus, EditableValue::Bool(agent.has_subconscious)));
|
||||
}
|
||||
}
|
||||
Category::Bifrost => {
|
||||
|
|
@ -614,6 +630,9 @@ impl SettingsView {
|
|||
|
||||
TuShowInterstitial => { if let EditableValue::Bool(v) = value { self.config.tui.show_interstitial = v; } }
|
||||
TuCennoThreshold => { if let EditableValue::Uint(v) = value { self.config.tui.cenno_word_threshold = v as usize; } }
|
||||
|
||||
// Read-only diagnostic fields — no-op on apply.
|
||||
AgAgentId | AgSubconsciousId | AgMemoryPath | AgSubconsciousPath | AgSubconsciousStatus => {}
|
||||
}
|
||||
self.dirty = true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue