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:
Kent Overstreet 2026-04-02 00:32:23 -04:00
parent c72eb4d528
commit a0245c1279
4 changed files with 364 additions and 115 deletions

View file

@ -50,7 +50,11 @@ pub async fn stream_events(
let url = format!("{}/chat/completions", base_url);
let msg_count = request.messages.len();
let debug_label = format!("{} messages, model={}", msg_count, model);
let pri_label = match priority {
Some(p) => format!(", priority={}", p),
None => String::new(),
};
let debug_label = format!("{} messages, model={}{}", msg_count, model, pri_label);
let mut response = super::send_and_check(
client,

View file

@ -301,6 +301,8 @@ impl Agent {
// Flush display_buf except a tail that could be
// a partial "<tool_call>" (10 chars).
let safe = display_buf.len().saturating_sub(10);
// Find a char boundary at or before safe
let safe = display_buf.floor_char_boundary(safe);
if safe > 0 {
let flush = display_buf[..safe].to_string();
display_buf = display_buf[safe..].to_string();

View file

@ -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);