Refactor hook: split agent orchestration from formatting
- Remove POC_AGENT early return (was from old claude -p era) - Split hook into run_agent_cycles() -> AgentCycleOutput (returns memory keys + reflection) and format_agent_output() (renders for Claude Code injection). poc-agent can call run_agent_cycles directly and handle output its own way. - Fix UTF-8 panic in runner.rs display_buf slicing (floor_char_boundary) - Add priority debug label to API requests - Wire up F2 agents screen: live pid status, output files, hook log tail, arrow key navigation, Enter for log detail view Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
c72eb4d528
commit
a0245c1279
4 changed files with 364 additions and 115 deletions
291
src/agent/tui.rs
291
src/agent/tui.rs
|
|
@ -10,6 +10,8 @@
|
|||
// handles rendering. Input is processed from crossterm key events.
|
||||
|
||||
const SCREEN_LEGEND: &str = " F1=main F2=agents F10=context ";
|
||||
const AGENT_NAMES: &[&str] = &["surface-observe", "journal", "reflect", "linker",
|
||||
"organize", "distill", "split"];
|
||||
|
||||
use crossterm::{
|
||||
event::{EnableMouseCapture, DisableMouseCapture, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, MouseButton},
|
||||
|
|
@ -342,6 +344,10 @@ pub struct App {
|
|||
context_info: Option<ContextInfo>,
|
||||
/// Live context state — shared with agent, read directly for debug screen.
|
||||
shared_context: SharedContextState,
|
||||
/// Agent screen: selected agent index.
|
||||
agent_selected: usize,
|
||||
/// Agent screen: viewing log for selected agent.
|
||||
agent_log_view: bool,
|
||||
}
|
||||
|
||||
/// Overlay screens toggled by F-keys.
|
||||
|
|
@ -402,6 +408,8 @@ impl App {
|
|||
debug_expanded: std::collections::HashSet::new(),
|
||||
context_info: None,
|
||||
shared_context,
|
||||
agent_selected: 0,
|
||||
agent_log_view: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -542,48 +550,85 @@ impl App {
|
|||
}
|
||||
KeyCode::F(10) => { self.set_overlay(Overlay::Context); return; }
|
||||
KeyCode::F(2) => { self.set_overlay(Overlay::Agents); return; }
|
||||
KeyCode::Up => {
|
||||
let cs = self.read_context_state();
|
||||
let n = self.debug_item_count(&cs);
|
||||
if n > 0 {
|
||||
self.debug_selected = Some(match self.debug_selected {
|
||||
None => n - 1,
|
||||
Some(0) => 0,
|
||||
Some(i) => i - 1,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
let cs = self.read_context_state();
|
||||
let n = self.debug_item_count(&cs);
|
||||
if n > 0 {
|
||||
self.debug_selected = Some(match self.debug_selected {
|
||||
None => 0,
|
||||
Some(i) if i >= n - 1 => n - 1,
|
||||
Some(i) => i + 1,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyCode::PageUp => { self.debug_scroll = self.debug_scroll.saturating_sub(10); return; }
|
||||
KeyCode::PageDown => { self.debug_scroll += 10; return; }
|
||||
KeyCode::Right | KeyCode::Enter => {
|
||||
// Expand selected section
|
||||
if let Some(idx) = self.debug_selected {
|
||||
self.debug_expanded.insert(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// Collapse selected section
|
||||
if let Some(idx) = self.debug_selected {
|
||||
self.debug_expanded.remove(&idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Screen-specific key handling
|
||||
match self.overlay {
|
||||
Some(Overlay::Agents) => {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
self.agent_selected = self.agent_selected.saturating_sub(1);
|
||||
self.debug_scroll = 0;
|
||||
return;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
self.agent_selected = (self.agent_selected + 1).min(AGENT_NAMES.len() - 1);
|
||||
self.debug_scroll = 0;
|
||||
return;
|
||||
}
|
||||
KeyCode::Enter | KeyCode::Right => {
|
||||
self.agent_log_view = true;
|
||||
self.debug_scroll = 0;
|
||||
return;
|
||||
}
|
||||
KeyCode::Left | KeyCode::Esc => {
|
||||
if self.agent_log_view {
|
||||
self.agent_log_view = false;
|
||||
self.debug_scroll = 0;
|
||||
} else {
|
||||
self.overlay = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(Overlay::Context) => {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
let cs = self.read_context_state();
|
||||
let n = self.debug_item_count(&cs);
|
||||
if n > 0 {
|
||||
self.debug_selected = Some(match self.debug_selected {
|
||||
None => n - 1,
|
||||
Some(0) => 0,
|
||||
Some(i) => i - 1,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
let cs = self.read_context_state();
|
||||
let n = self.debug_item_count(&cs);
|
||||
if n > 0 {
|
||||
self.debug_selected = Some(match self.debug_selected {
|
||||
None => 0,
|
||||
Some(i) if i >= n - 1 => n - 1,
|
||||
Some(i) => i + 1,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyCode::Right | KeyCode::Enter => {
|
||||
if let Some(idx) = self.debug_selected {
|
||||
self.debug_expanded.insert(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
if let Some(idx) = self.debug_selected {
|
||||
self.debug_expanded.remove(&idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
match key.code {
|
||||
|
|
@ -984,13 +1029,61 @@ impl App {
|
|||
}
|
||||
|
||||
fn draw_agents(&self, frame: &mut Frame, size: Rect) {
|
||||
let output_dir = crate::store::memory_dir().join("agent-output");
|
||||
|
||||
if self.agent_log_view {
|
||||
self.draw_agent_log(frame, size, &output_dir);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let section = Style::default().fg(Color::Yellow);
|
||||
let dim = Style::default().fg(Color::DarkGray);
|
||||
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
|
||||
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::styled("── Subconscious Agents ──", section));
|
||||
lines.push(Line::styled(" (↑/↓ select, Enter/→ view log, Esc back)", hint));
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::raw(" (not yet wired — will show surface, observe, reflect, journal status)"));
|
||||
|
||||
for (i, &name) in AGENT_NAMES.iter().enumerate() {
|
||||
let agent_dir = output_dir.join(name);
|
||||
let live = crate::subconscious::knowledge::scan_pid_files(&agent_dir, 0);
|
||||
let selected = i == self.agent_selected;
|
||||
|
||||
let prefix = if selected { "▸ " } else { " " };
|
||||
let bg = if selected { Style::default().bg(Color::DarkGray) } else { Style::default() };
|
||||
|
||||
if live.is_empty() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!("{}{:<20}", prefix, name), bg.fg(Color::Gray)),
|
||||
Span::styled("○ idle", bg.fg(Color::DarkGray)),
|
||||
]));
|
||||
} else {
|
||||
for (phase, pid) in &live {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!("{}{:<20}", prefix, name), bg.fg(Color::Green)),
|
||||
Span::styled("● ", bg.fg(Color::Green)),
|
||||
Span::styled(format!("pid {} phase: {}", pid, phase), bg),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recent output
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::styled("── Recent Activity ──", section));
|
||||
lines.push(Line::raw(""));
|
||||
|
||||
for &name in AGENT_NAMES {
|
||||
let agent_dir = output_dir.join(name);
|
||||
if let Some((file, ago)) = Self::most_recent_file(&agent_dir) {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {:<20}", name), dim),
|
||||
Span::raw(format!("{} ({})", file, ago)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
let block = Block::default()
|
||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
||||
|
|
@ -1004,6 +1097,126 @@ impl App {
|
|||
frame.render_widget(para, size);
|
||||
}
|
||||
|
||||
fn draw_agent_log(&self, frame: &mut Frame, size: Rect, output_dir: &std::path::Path) {
|
||||
let name = AGENT_NAMES.get(self.agent_selected).unwrap_or(&"?");
|
||||
let agent_dir = output_dir.join(name);
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let section = Style::default().fg(Color::Yellow);
|
||||
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
|
||||
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::styled(format!("── {} ──", name), section));
|
||||
lines.push(Line::styled(" (Esc/← back, PgUp/PgDn scroll)", hint));
|
||||
lines.push(Line::raw(""));
|
||||
|
||||
// Show pid status
|
||||
let live = crate::subconscious::knowledge::scan_pid_files(&agent_dir, 0);
|
||||
if live.is_empty() {
|
||||
lines.push(Line::styled(" Status: idle", Style::default().fg(Color::DarkGray)));
|
||||
} else {
|
||||
for (phase, pid) in &live {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" Status: ", Style::default()),
|
||||
Span::styled(format!("● running pid {} phase: {}", pid, phase),
|
||||
Style::default().fg(Color::Green)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
lines.push(Line::raw(""));
|
||||
|
||||
// Show output files
|
||||
lines.push(Line::styled("── Output Files ──", section));
|
||||
let mut files: Vec<_> = std::fs::read_dir(&agent_dir)
|
||||
.into_iter().flatten().flatten()
|
||||
.filter(|e| {
|
||||
let n = e.file_name().to_string_lossy().to_string();
|
||||
!n.starts_with("pid-") && !n.starts_with("transcript-offset")
|
||||
&& !n.starts_with("chunks-") && !n.starts_with("seen")
|
||||
})
|
||||
.collect();
|
||||
files.sort_by_key(|e| std::cmp::Reverse(
|
||||
e.metadata().ok().and_then(|m| m.modified().ok())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
));
|
||||
|
||||
for entry in files.iter().take(10) {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let ago = entry.metadata().ok()
|
||||
.and_then(|m| m.modified().ok())
|
||||
.and_then(|t| t.elapsed().ok())
|
||||
.map(|d| Self::format_duration(d))
|
||||
.unwrap_or_else(|| "?".into());
|
||||
let size = entry.metadata().ok().map(|m| m.len()).unwrap_or(0);
|
||||
lines.push(Line::raw(format!(" {:<30} {:>6}B {}", name, size, ago)));
|
||||
}
|
||||
|
||||
// Show hook log tail
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::styled("── Hook Log ──", section));
|
||||
|
||||
let log_dir = dirs::home_dir().unwrap_or_default().join(".consciousness/logs");
|
||||
// Find latest hook log
|
||||
if let Ok(mut entries) = std::fs::read_dir(&log_dir) {
|
||||
let mut logs: Vec<_> = entries.by_ref().flatten()
|
||||
.filter(|e| e.file_name().to_string_lossy().starts_with("hook-"))
|
||||
.collect();
|
||||
logs.sort_by_key(|e| std::cmp::Reverse(
|
||||
e.metadata().ok().and_then(|m| m.modified().ok())
|
||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
));
|
||||
if let Some(log_entry) = logs.first() {
|
||||
if let Ok(content) = std::fs::read_to_string(log_entry.path()) {
|
||||
// Show last ~30 lines
|
||||
let log_lines: Vec<&str> = content.lines().collect();
|
||||
let start = log_lines.len().saturating_sub(30);
|
||||
for line in &log_lines[start..] {
|
||||
lines.push(Line::raw(format!(" {}", line)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let block = Block::default()
|
||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
||||
.title_top(Line::from(format!(" {} ", name)).right_aligned())
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan));
|
||||
|
||||
let para = Paragraph::new(lines)
|
||||
.block(block)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((self.debug_scroll, 0));
|
||||
frame.render_widget(para, size);
|
||||
}
|
||||
|
||||
fn most_recent_file(dir: &std::path::Path) -> Option<(String, String)> {
|
||||
let entries = std::fs::read_dir(dir).ok()?;
|
||||
let mut latest: Option<(String, std::time::SystemTime)> = None;
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("pid-") || name.starts_with("transcript-offset") { continue; }
|
||||
if let Ok(meta) = entry.metadata() {
|
||||
if let Ok(modified) = meta.modified() {
|
||||
if latest.as_ref().map_or(true, |(_, t)| modified > *t) {
|
||||
latest = Some((name, modified));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
latest.map(|(name, time)| {
|
||||
let ago = time.elapsed().map(|d| Self::format_duration(d))
|
||||
.unwrap_or_else(|_| "?".into());
|
||||
(name, ago)
|
||||
})
|
||||
}
|
||||
|
||||
fn format_duration(d: std::time::Duration) -> String {
|
||||
let secs = d.as_secs();
|
||||
if secs < 60 { format!("{}s ago", secs) }
|
||||
else if secs < 3600 { format!("{}m ago", secs / 60) }
|
||||
else { format!("{}h ago", secs / 3600) }
|
||||
}
|
||||
|
||||
fn draw_debug(&self, frame: &mut Frame, size: Rect) {
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let section = Style::default().fg(Color::Yellow);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue