use std::time::{Duration, Instant};
use crate::monitor::dashboard::{MemoryData, PalaceRow};
use crate::monitor::memory_client::{DrawerInfo, MemoryDetail};
use crate::monitor::tui_common::ThreeWaySortKey;
use crate::monitor::utils::{ActivityLog, DaemonStatus};
pub const RECALL_TOP_K: usize = 5;
pub const DRAWER_PAGE_SIZE: usize = 20;
pub const DREAM_BACKOFF_INITIAL: Duration = Duration::from_secs(5);
pub const DREAM_BACKOFF_MAX: Duration = Duration::from_secs(300);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MemoryFocus {
#[default]
List,
DrawerPane,
Input,
}
impl MemoryFocus {
pub fn next(self) -> Self {
match self {
Self::List => Self::DrawerPane,
Self::DrawerPane => Self::Input,
Self::Input => Self::List,
}
}
pub fn toggled(self) -> Self {
match self {
Self::List => Self::Input,
Self::Input => Self::List,
Self::DrawerPane => Self::List,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DreamBackoff {
pub(crate) next_allowed_at: Option<Instant>,
pub(crate) consecutive_failures: u32,
pub(crate) first_failure_logged: bool,
}
impl DreamBackoff {
pub fn new() -> Self {
Self::default()
}
pub fn ready(&self, now: Instant) -> bool {
match self.next_allowed_at {
Some(deadline) => now >= deadline,
None => true,
}
}
pub fn remaining(&self, now: Instant) -> Duration {
self.next_allowed_at
.and_then(|d| d.checked_duration_since(now))
.unwrap_or(Duration::ZERO)
}
pub fn record_success(&mut self) {
self.next_allowed_at = None;
self.consecutive_failures = 0;
self.first_failure_logged = false;
}
pub fn record_failure(&mut self, now: Instant) -> bool {
self.consecutive_failures = self.consecutive_failures.saturating_add(1);
let delay = backoff_delay(self.consecutive_failures);
self.next_allowed_at = Some(now + delay);
let should_log = !self.first_failure_logged;
self.first_failure_logged = true;
should_log
}
pub fn consecutive_failures(&self) -> u32 {
self.consecutive_failures
}
}
pub(crate) fn backoff_delay(n: u32) -> Duration {
let shift = n.saturating_sub(1).min(20); let multiplier: u64 = 1u64 << shift;
let secs = DREAM_BACKOFF_INITIAL
.as_secs()
.saturating_mul(multiplier)
.min(DREAM_BACKOFF_MAX.as_secs());
Duration::from_secs(secs)
}
#[derive(Debug, Clone, Default)]
pub struct DrawerListState {
pub palace_id: Option<String>,
pub drawers: Vec<DrawerInfo>,
pub offset: usize,
pub loading: bool,
pub last_error: Option<String>,
}
impl DrawerListState {
pub fn new() -> Self {
Self::default()
}
pub fn reset_for(&mut self, scope: Option<String>) {
self.palace_id = scope;
self.drawers.clear();
self.offset = 0;
self.loading = true;
self.last_error = None;
}
pub fn next_page(&mut self) {
if self.drawers.len() >= DRAWER_PAGE_SIZE {
self.offset = self.offset.saturating_add(DRAWER_PAGE_SIZE);
self.loading = true;
self.last_error = None;
}
}
pub fn prev_page(&mut self) {
if self.offset == 0 {
return;
}
self.offset = self.offset.saturating_sub(DRAWER_PAGE_SIZE);
self.loading = true;
self.last_error = None;
}
pub fn page(&self) -> usize {
self.offset / DRAWER_PAGE_SIZE.max(1)
}
}
#[derive(Debug, Clone)]
pub struct MemoryTuiState {
pub base_url: String,
pub daemon_status: DaemonStatus,
pub status: Option<MemoryData>,
pub palaces: Vec<PalaceRow>,
pub selected: usize,
pub scroll_offset: usize,
pub log: ActivityLog,
pub input: String,
pub focus: MemoryFocus,
pub show_help: bool,
pub filter: String,
pub filter_active: bool,
pub sort_key: ThreeWaySortKey,
pub group_by_project: bool,
pub dream_backoff: DreamBackoff,
pub drawer_list: DrawerListState,
pub drawer_cursor: usize,
pub drawer_detail_open: bool,
pub drawer_detail_idx: usize,
pub drawer_detail_memories: Vec<MemoryDetail>,
pub drawer_detail_scroll: usize,
pub drawer_detail_loading: bool,
}
impl MemoryTuiState {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
daemon_status: DaemonStatus::Connecting,
status: None,
palaces: Vec::new(),
selected: 0,
scroll_offset: 0,
log: ActivityLog::new(),
input: String::new(),
focus: MemoryFocus::List,
show_help: false,
filter: String::new(),
filter_active: false,
sort_key: ThreeWaySortKey::default(),
group_by_project: false,
dream_backoff: DreamBackoff::new(),
drawer_list: DrawerListState::new(),
drawer_cursor: 0,
drawer_detail_open: false,
drawer_detail_idx: 0,
drawer_detail_memories: Vec::new(),
drawer_detail_scroll: 0,
drawer_detail_loading: false,
}
}
pub fn toggle_focus(&mut self) {
self.focus = self.focus.toggled();
}
pub fn cycle_focus(&mut self) {
self.focus = self.focus.next();
if self.focus == MemoryFocus::List {
self.drawer_cursor = 0;
}
}
pub fn drawer_cursor_up(&mut self) {
self.drawer_cursor = self.drawer_cursor.saturating_sub(1);
}
pub fn drawer_cursor_down(&mut self) {
let len = self.drawer_list.drawers.len();
if len == 0 {
self.drawer_cursor = 0;
return;
}
if self.drawer_cursor + 1 < len {
self.drawer_cursor += 1;
}
}
pub fn clamp_drawer_cursor(&mut self) {
let len = self.drawer_list.drawers.len();
if len == 0 {
self.drawer_cursor = 0;
} else if self.drawer_cursor >= len {
self.drawer_cursor = len - 1;
}
}
pub fn close_drawer_detail(&mut self) {
self.drawer_detail_open = false;
self.drawer_detail_memories.clear();
self.drawer_detail_scroll = 0;
self.drawer_detail_loading = false;
}
pub fn select_up(&mut self) {
self.selected = self.selected.saturating_sub(1);
}
pub fn select_down(&mut self) {
if self.selected < self.last_row() {
self.selected += 1;
}
}
pub fn last_row(&self) -> usize {
self.palaces.len()
}
pub fn clamp_selection(&mut self) {
if self.selected > self.last_row() {
self.selected = self.last_row();
}
}
pub fn sync_scroll(&mut self, visible: usize) {
let cursor = self.selected;
self.sync_scroll_to(cursor, visible);
}
pub fn sync_scroll_to(&mut self, cursor_row: usize, visible: usize) {
let window = visible.max(1);
if cursor_row >= self.scroll_offset + window {
self.scroll_offset = cursor_row + 1 - window;
} else if cursor_row < self.scroll_offset {
self.scroll_offset = cursor_row;
}
}
pub fn is_all_selected(&self) -> bool {
self.selected == 0
}
pub fn selected_id(&self) -> Option<&str> {
if self.selected == 0 {
return None;
}
self.palaces.get(self.selected - 1).map(|p| p.id.as_str())
}
pub fn clamp_to_visible(&mut self) {
if self.selected == 0 {
return;
}
let Some(current_id) = self.palaces.get(self.selected - 1).map(|p| p.id.clone()) else {
self.selected = 0;
return;
};
let ids = crate::monitor::memory_tui::view::visible_palace_ids(self);
if !ids.iter().any(|id| id == ¤t_id) {
self.selected = 0;
}
}
pub fn scope_filter(&self) -> Option<&str> {
self.selected_id()
}
}