2026-04-04 02:46:32 -04:00
|
|
|
// user/ — User interface layer
|
2026-03-25 00:52:41 -04:00
|
|
|
//
|
2026-04-04 02:46:32 -04:00
|
|
|
// TUI, UI channel, parsing. The cognitive layer (session state
|
|
|
|
|
// machine, DMN, identity) lives in mind/.
|
2026-03-25 00:52:41 -04:00
|
|
|
|
|
|
|
|
pub mod ui_channel;
|
mind: split event loop — Mind and UI run independently
Mind::run() owns the cognitive event loop: user input, turn results,
DMN ticks, hotkey actions. The UI event loop (user/event_loop.rs) owns
the terminal: key events, render ticks, channel status display.
They communicate through channels: UI sends MindMessage (user input,
hotkey actions) to Mind. Mind sends UiMessage (status, info) to UI.
UI reads shared state (active tools, context) directly for rendering.
Removes direct coupling between Mind and App:
- cycle_reasoning no longer takes &mut App
- AdjustSampling updates agent only, UI reads from shared state
- /quit handled by UI directly, not routed through Mind
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 02:11:32 -04:00
|
|
|
pub mod event_loop;
|
2026-04-04 02:46:32 -04:00
|
|
|
|
|
|
|
|
pub mod chat;
|
|
|
|
|
pub mod context;
|
|
|
|
|
pub mod subconscious;
|
|
|
|
|
pub mod unconscious;
|
|
|
|
|
pub mod thalamus;
|
|
|
|
|
|
|
|
|
|
// --- TUI infrastructure (moved from tui/mod.rs) ---
|
|
|
|
|
|
2026-04-05 06:22:31 -04:00
|
|
|
use ratatui::crossterm::{
|
2026-04-05 18:57:54 -04:00
|
|
|
event::{EnableMouseCapture, DisableMouseCapture, KeyCode, KeyEvent, KeyModifiers},
|
2026-04-04 02:46:32 -04:00
|
|
|
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
|
|
|
|
ExecutableCommand,
|
|
|
|
|
};
|
|
|
|
|
use ratatui::{
|
|
|
|
|
backend::CrosstermBackend,
|
|
|
|
|
style::{Color, Style},
|
|
|
|
|
text::{Line, Span},
|
|
|
|
|
};
|
|
|
|
|
use std::io;
|
|
|
|
|
|
2026-04-05 18:57:54 -04:00
|
|
|
use crate::user::ui_channel::{ContextInfo, SharedContextState, StatusInfo};
|
2026-04-04 02:46:32 -04:00
|
|
|
|
user: ScreenView trait, overlay screens extracted from App
Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.
- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll
Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.
App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.
InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 17:54:40 -04:00
|
|
|
/// Build the screen legend from the screen table.
|
|
|
|
|
pub(crate) fn screen_legend() -> String {
|
|
|
|
|
// Built from the SCREENS table in event_loop
|
|
|
|
|
" F1=interact F2=conscious F3=subconscious F4=unconscious F5=thalamus ".to_string()
|
|
|
|
|
}
|
2026-04-04 02:46:32 -04:00
|
|
|
|
|
|
|
|
pub(crate) fn strip_ansi(text: &str) -> String {
|
|
|
|
|
let mut out = String::with_capacity(text.len());
|
|
|
|
|
let mut chars = text.chars().peekable();
|
|
|
|
|
while let Some(ch) = chars.next() {
|
|
|
|
|
if ch == '\x1b' {
|
|
|
|
|
if chars.peek() == Some(&'[') {
|
|
|
|
|
chars.next();
|
|
|
|
|
while let Some(&c) = chars.peek() {
|
|
|
|
|
if c.is_ascii() && (0x20..=0x3F).contains(&(c as u8)) {
|
|
|
|
|
chars.next();
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(&c) = chars.peek() {
|
|
|
|
|
if c.is_ascii() && (0x40..=0x7E).contains(&(c as u8)) {
|
|
|
|
|
chars.next();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if let Some(&c) = chars.peek() {
|
|
|
|
|
if c.is_ascii() && (0x40..=0x5F).contains(&(c as u8)) {
|
|
|
|
|
chars.next();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
out.push(ch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn is_zero_width(ch: char) -> bool {
|
|
|
|
|
matches!(ch,
|
|
|
|
|
'\u{200B}'..='\u{200F}' |
|
|
|
|
|
'\u{2028}'..='\u{202F}' |
|
|
|
|
|
'\u{2060}'..='\u{2069}' |
|
|
|
|
|
'\u{FEFF}'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Which pane receives scroll keys.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
pub(crate) enum ActivePane {
|
|
|
|
|
Autonomous,
|
|
|
|
|
Conversation,
|
|
|
|
|
Tools,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MAX_PANE_LINES: usize = 10_000;
|
|
|
|
|
|
|
|
|
|
/// Turn marker for the conversation pane gutter.
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Default)]
|
|
|
|
|
pub(crate) enum Marker {
|
|
|
|
|
#[default]
|
|
|
|
|
None,
|
|
|
|
|
User,
|
|
|
|
|
Assistant,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct PaneState {
|
|
|
|
|
pub(crate) lines: Vec<Line<'static>>,
|
|
|
|
|
pub(crate) markers: Vec<Marker>,
|
|
|
|
|
pub(crate) current_line: String,
|
|
|
|
|
pub(crate) current_color: Color,
|
|
|
|
|
pub(crate) md_buffer: String,
|
|
|
|
|
pub(crate) use_markdown: bool,
|
|
|
|
|
pub(crate) pending_marker: Marker,
|
|
|
|
|
pub(crate) scroll: u16,
|
|
|
|
|
pub(crate) pinned: bool,
|
|
|
|
|
pub(crate) last_total_lines: u16,
|
|
|
|
|
pub(crate) last_height: u16,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PaneState {
|
|
|
|
|
fn new(use_markdown: bool) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
lines: Vec::new(), markers: Vec::new(),
|
|
|
|
|
current_line: String::new(), current_color: Color::Reset,
|
|
|
|
|
md_buffer: String::new(), use_markdown,
|
|
|
|
|
pending_marker: Marker::None, scroll: 0, pinned: false,
|
|
|
|
|
last_total_lines: 0, last_height: 20,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn evict(&mut self) {
|
|
|
|
|
if self.lines.len() > MAX_PANE_LINES {
|
|
|
|
|
let excess = self.lines.len() - MAX_PANE_LINES;
|
|
|
|
|
self.lines.drain(..excess);
|
|
|
|
|
self.markers.drain(..excess);
|
|
|
|
|
self.scroll = self.scroll.saturating_sub(excess as u16);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn append_text(&mut self, text: &str) {
|
|
|
|
|
let clean = strip_ansi(text);
|
|
|
|
|
if self.use_markdown {
|
|
|
|
|
self.md_buffer.push_str(&clean);
|
|
|
|
|
} else {
|
|
|
|
|
for ch in clean.chars() {
|
|
|
|
|
if ch == '\n' {
|
|
|
|
|
let line = std::mem::take(&mut self.current_line);
|
|
|
|
|
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
|
|
|
|
|
self.markers.push(Marker::None);
|
|
|
|
|
} else if ch == '\t' {
|
|
|
|
|
self.current_line.push_str(" ");
|
|
|
|
|
} else if ch.is_control() || is_zero_width(ch) {
|
|
|
|
|
} else {
|
|
|
|
|
self.current_line.push(ch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.evict();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn flush_pending(&mut self) {
|
|
|
|
|
if self.use_markdown && !self.md_buffer.is_empty() {
|
|
|
|
|
let parsed = parse_markdown(&self.md_buffer);
|
|
|
|
|
for (i, line) in parsed.into_iter().enumerate() {
|
|
|
|
|
let marker = if i == 0 { std::mem::take(&mut self.pending_marker) } else { Marker::None };
|
|
|
|
|
self.lines.push(line);
|
|
|
|
|
self.markers.push(marker);
|
|
|
|
|
}
|
|
|
|
|
self.md_buffer.clear();
|
|
|
|
|
}
|
|
|
|
|
if !self.current_line.is_empty() {
|
|
|
|
|
let line = std::mem::take(&mut self.current_line);
|
|
|
|
|
self.lines.push(Line::styled(line, Style::default().fg(self.current_color)));
|
|
|
|
|
self.markers.push(std::mem::take(&mut self.pending_marker));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn push_line(&mut self, line: String, color: Color) {
|
|
|
|
|
self.push_line_with_marker(line, color, Marker::None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn push_line_with_marker(&mut self, line: String, color: Color, marker: Marker) {
|
|
|
|
|
self.flush_pending();
|
|
|
|
|
self.lines.push(Line::styled(strip_ansi(&line), Style::default().fg(color)));
|
|
|
|
|
self.markers.push(marker);
|
|
|
|
|
self.evict();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn scroll_up(&mut self, n: u16) {
|
|
|
|
|
self.scroll = self.scroll.saturating_sub(n);
|
|
|
|
|
self.pinned = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn scroll_down(&mut self, n: u16) {
|
|
|
|
|
let max = self.last_total_lines.saturating_sub(self.last_height);
|
|
|
|
|
self.scroll = (self.scroll + n).min(max);
|
|
|
|
|
if self.scroll >= max { self.pinned = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn all_lines(&self) -> Vec<Line<'static>> {
|
|
|
|
|
let (lines, _) = self.all_lines_with_markers();
|
|
|
|
|
lines
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn all_lines_with_markers(&self) -> (Vec<Line<'static>>, Vec<Marker>) {
|
|
|
|
|
let mut lines: Vec<Line<'static>> = self.lines.clone();
|
|
|
|
|
let mut markers: Vec<Marker> = self.markers.clone();
|
|
|
|
|
if self.use_markdown && !self.md_buffer.is_empty() {
|
|
|
|
|
let parsed = parse_markdown(&self.md_buffer);
|
|
|
|
|
let count = parsed.len();
|
|
|
|
|
lines.extend(parsed);
|
|
|
|
|
if count > 0 {
|
|
|
|
|
markers.push(self.pending_marker);
|
|
|
|
|
markers.extend(std::iter::repeat(Marker::None).take(count - 1));
|
|
|
|
|
}
|
|
|
|
|
} else if !self.current_line.is_empty() {
|
|
|
|
|
lines.push(Line::styled(self.current_line.clone(), Style::default().fg(self.current_color)));
|
|
|
|
|
markers.push(self.pending_marker);
|
|
|
|
|
}
|
|
|
|
|
(lines, markers)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn new_textarea(lines: Vec<String>) -> tui_textarea::TextArea<'static> {
|
|
|
|
|
let mut ta = tui_textarea::TextArea::new(lines);
|
|
|
|
|
ta.set_cursor_line_style(Style::default());
|
|
|
|
|
ta.set_wrap_mode(tui_textarea::WrapMode::Word);
|
|
|
|
|
ta
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn parse_markdown(md: &str) -> Vec<Line<'static>> {
|
|
|
|
|
tui_markdown::from_str(md)
|
|
|
|
|
.lines
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|line| {
|
|
|
|
|
let spans: Vec<Span<'static>> = line.spans.into_iter()
|
|
|
|
|
.map(|span| Span::styled(span.content.into_owned(), span.style))
|
|
|
|
|
.collect();
|
|
|
|
|
let mut result = Line::from(spans).style(line.style);
|
|
|
|
|
result.alignment = line.alignment;
|
|
|
|
|
result
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
user: ScreenView trait, overlay screens extracted from App
Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.
- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll
Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.
App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.
InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 17:54:40 -04:00
|
|
|
/// Action returned from a screen's tick method.
|
|
|
|
|
pub enum ScreenAction {
|
|
|
|
|
/// Switch to screen at this index
|
|
|
|
|
Switch(usize),
|
|
|
|
|
/// Send a hotkey action to the Mind
|
|
|
|
|
Hotkey(HotkeyAction),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A screen that can draw itself and handle input.
|
2026-04-05 17:59:33 -04:00
|
|
|
pub(crate) trait ScreenView: Send {
|
user: ScreenView trait, overlay screens extracted from App
Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.
- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll
Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.
App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.
InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 17:54:40 -04:00
|
|
|
fn tick(&mut self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect,
|
2026-04-05 18:57:54 -04:00
|
|
|
key: Option<ratatui::crossterm::event::KeyEvent>, app: &mut App) -> Option<ScreenAction>;
|
user: ScreenView trait, overlay screens extracted from App
Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.
- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll
Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.
App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.
InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 17:54:40 -04:00
|
|
|
fn label(&self) -> &'static str;
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum HotkeyAction {
|
|
|
|
|
CycleReasoning, KillProcess, Interrupt, CycleAutonomy,
|
2026-04-04 14:06:42 -04:00
|
|
|
/// Adjust a sampling parameter: (param_index, delta)
|
|
|
|
|
/// 0=temperature, 1=top_p, 2=top_k
|
|
|
|
|
AdjustSampling(usize, f32),
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct IdleInfo {
|
|
|
|
|
pub user_present: bool,
|
|
|
|
|
pub since_activity: f64,
|
|
|
|
|
pub activity_ewma: f64,
|
|
|
|
|
pub block_reason: String,
|
|
|
|
|
pub dreaming: bool,
|
|
|
|
|
pub sleeping: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct ChannelStatus {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub connected: bool,
|
|
|
|
|
pub unread: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct App {
|
|
|
|
|
pub(crate) status: StatusInfo,
|
|
|
|
|
pub(crate) activity: String,
|
|
|
|
|
pub running_processes: u32,
|
|
|
|
|
pub reasoning_effort: String,
|
2026-04-04 13:48:24 -04:00
|
|
|
pub temperature: f32,
|
|
|
|
|
pub top_p: f32,
|
|
|
|
|
pub top_k: u32,
|
2026-04-04 02:46:32 -04:00
|
|
|
pub(crate) active_tools: crate::user::ui_channel::SharedActiveTools,
|
|
|
|
|
pub should_quit: bool,
|
|
|
|
|
pub submitted: Vec<String>,
|
|
|
|
|
pub hotkey_actions: Vec<HotkeyAction>,
|
|
|
|
|
pub(crate) context_info: Option<ContextInfo>,
|
|
|
|
|
pub(crate) shared_context: SharedContextState,
|
|
|
|
|
pub(crate) agent_state: Vec<crate::subconscious::subconscious::AgentSnapshot>,
|
|
|
|
|
pub(crate) channel_status: Vec<ChannelStatus>,
|
|
|
|
|
pub(crate) idle_info: Option<IdleInfo>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl App {
|
|
|
|
|
pub fn new(model: String, shared_context: SharedContextState, active_tools: crate::user::ui_channel::SharedActiveTools) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
status: StatusInfo {
|
|
|
|
|
dmn_state: "resting".into(), dmn_turns: 0, dmn_max_turns: 20,
|
|
|
|
|
prompt_tokens: 0, completion_tokens: 0, model,
|
|
|
|
|
turn_tools: 0, context_budget: String::new(),
|
|
|
|
|
},
|
|
|
|
|
activity: String::new(),
|
2026-04-05 18:57:54 -04:00
|
|
|
running_processes: 0,
|
2026-04-04 02:46:32 -04:00
|
|
|
reasoning_effort: "none".to_string(),
|
2026-04-04 13:48:24 -04:00
|
|
|
temperature: 0.6,
|
|
|
|
|
top_p: 0.95,
|
|
|
|
|
top_k: 20,
|
2026-04-05 18:57:54 -04:00
|
|
|
active_tools,
|
2026-04-04 02:46:32 -04:00
|
|
|
should_quit: false, submitted: Vec::new(), hotkey_actions: Vec::new(),
|
|
|
|
|
context_info: None, shared_context,
|
user: ScreenView trait, overlay screens extracted from App
Convert F2-F5 screens to ScreenView trait with tick() method.
Each screen owns its view state (scroll, selection, expanded).
State persists across screen switches.
- ThalamusScreen: owns sampling_selected, scroll
- ConsciousScreen: owns scroll, selected, expanded
- SubconsciousScreen: owns selected, log_view, scroll
- UnconsciousScreen: owns scroll
Removed from App: Screen enum, debug_scroll, debug_selected,
debug_expanded, agent_selected, agent_log_view, sampling_selected,
set_screen(), per-screen key handling, draw dispatch.
App now only draws the interact (F1) screen. Overlay screens are
drawn by the event loop via ScreenView::tick. F-key routing and
screen instantiation to be wired in event_loop next.
InteractScreen (state-driven, reading from agent entries) is the
next step — will eliminate the input display race condition.
Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
2026-04-05 17:54:40 -04:00
|
|
|
agent_state: Vec::new(),
|
|
|
|
|
channel_status: Vec::new(), idle_info: None,
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 18:57:54 -04:00
|
|
|
pub fn handle_global_key_old(&mut self, _key: KeyEvent) -> bool { false } // placeholder
|
2026-04-04 02:46:32 -04:00
|
|
|
|
2026-04-05 18:57:54 -04:00
|
|
|
/// Handle global keys only (Ctrl combos).
|
|
|
|
|
pub fn handle_global_key(&mut self, key: KeyEvent) -> bool {
|
2026-04-04 02:46:32 -04:00
|
|
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
|
|
|
match key.code {
|
2026-04-05 18:57:54 -04:00
|
|
|
KeyCode::Char('c') => { self.should_quit = true; return true; }
|
|
|
|
|
KeyCode::Char('r') => { self.hotkey_actions.push(HotkeyAction::CycleReasoning); return true; }
|
|
|
|
|
KeyCode::Char('k') => { self.hotkey_actions.push(HotkeyAction::KillProcess); return true; }
|
|
|
|
|
KeyCode::Char('p') => { self.hotkey_actions.push(HotkeyAction::CycleAutonomy); return true; }
|
2026-04-04 02:46:32 -04:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 18:57:54 -04:00
|
|
|
false
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_channel_status(&mut self, channels: Vec<(String, bool, u32)>) {
|
|
|
|
|
self.channel_status = channels.into_iter()
|
|
|
|
|
.map(|(name, connected, unread)| ChannelStatus { name, connected, unread })
|
|
|
|
|
.collect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_idle(&mut self, state: &crate::thalamus::idle::State) {
|
|
|
|
|
self.idle_info = Some(IdleInfo {
|
|
|
|
|
user_present: state.user_present(), since_activity: state.since_activity(),
|
|
|
|
|
activity_ewma: state.activity_ewma, block_reason: state.block_reason().to_string(),
|
|
|
|
|
dreaming: state.dreaming, sleeping: state.sleep_until.is_some(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 18:57:54 -04:00
|
|
|
pub fn init_terminal() -> io::Result<ratatui::Terminal<CrosstermBackend<io::Stdout>>> {
|
2026-04-04 02:46:32 -04:00
|
|
|
terminal::enable_raw_mode()?;
|
|
|
|
|
let mut stdout = io::stdout();
|
|
|
|
|
stdout.execute(EnterAlternateScreen)?;
|
|
|
|
|
stdout.execute(EnableMouseCapture)?;
|
|
|
|
|
let backend = CrosstermBackend::new(stdout);
|
2026-04-05 18:57:54 -04:00
|
|
|
ratatui::Terminal::new(backend)
|
2026-04-04 02:46:32 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 18:57:54 -04:00
|
|
|
pub fn restore_terminal(terminal: &mut ratatui::Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
|
2026-04-04 02:46:32 -04:00
|
|
|
terminal::disable_raw_mode()?;
|
|
|
|
|
terminal.backend_mut().execute(DisableMouseCapture)?;
|
|
|
|
|
terminal.backend_mut().execute(LeaveAlternateScreen)?;
|
|
|
|
|
terminal.show_cursor()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- CLI ---
|
|
|
|
|
|
|
|
|
|
use clap::{Parser, Subcommand};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
|
#[command(name = "consciousness", about = "Substrate-independent AI agent")]
|
|
|
|
|
pub struct CliArgs {
|
|
|
|
|
/// Select active backend ("anthropic" or "openrouter")
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub backend: Option<String>,
|
|
|
|
|
|
|
|
|
|
/// Model override
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
pub model: Option<String>,
|
|
|
|
|
|
|
|
|
|
/// API key override
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub api_key: Option<String>,
|
|
|
|
|
|
|
|
|
|
/// Base URL override
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub api_base: Option<String>,
|
|
|
|
|
|
|
|
|
|
/// Enable debug logging
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub debug: bool,
|
|
|
|
|
|
|
|
|
|
/// Print effective config with provenance and exit
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub show_config: bool,
|
|
|
|
|
|
|
|
|
|
/// Override all prompt assembly with this file
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub system_prompt_file: Option<PathBuf>,
|
|
|
|
|
|
|
|
|
|
/// Project memory directory
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub memory_project: Option<PathBuf>,
|
|
|
|
|
|
|
|
|
|
/// Max consecutive DMN turns
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub dmn_max_turns: Option<u32>,
|
|
|
|
|
|
2026-04-04 23:06:25 -04:00
|
|
|
/// Disable background agents (surface, observe, scoring)
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
pub no_agents: bool,
|
|
|
|
|
|
2026-04-04 02:46:32 -04:00
|
|
|
#[command(subcommand)]
|
|
|
|
|
pub command: Option<SubCmd>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
|
pub enum SubCmd {
|
|
|
|
|
/// Print new output since last read and exit
|
|
|
|
|
Read {
|
|
|
|
|
/// Stream output continuously instead of exiting
|
|
|
|
|
#[arg(short, long)]
|
|
|
|
|
follow: bool,
|
|
|
|
|
/// Block until a complete response is received, then exit
|
|
|
|
|
#[arg(long)]
|
|
|
|
|
block: bool,
|
|
|
|
|
},
|
|
|
|
|
/// Send a message to the running agent
|
|
|
|
|
Write {
|
|
|
|
|
/// The message to send
|
|
|
|
|
message: Vec<String>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
|
pub async fn main() {
|
|
|
|
|
let cli = CliArgs::parse();
|
|
|
|
|
|
|
|
|
|
if cli.show_config {
|
|
|
|
|
match crate::config::load_app(&cli) {
|
|
|
|
|
Ok((app, figment)) => crate::config::show_config(&app, &figment),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Error loading config: {:#}", e);
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 04:29:56 -04:00
|
|
|
if let Err(e) = crate::user::event_loop::start(cli).await {
|
2026-04-05 06:22:31 -04:00
|
|
|
let _ = ratatui::crossterm::terminal::disable_raw_mode();
|
|
|
|
|
let _ = ratatui::crossterm::execute!(
|
2026-04-04 02:46:32 -04:00
|
|
|
std::io::stdout(),
|
2026-04-05 06:22:31 -04:00
|
|
|
ratatui::crossterm::terminal::LeaveAlternateScreen
|
2026-04-04 02:46:32 -04:00
|
|
|
);
|
|
|
|
|
eprintln!("Error: {:#}", e);
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|