use std::time::Duration;
use crate::{
context::Context,
core::TokenUsageInfo,
events::{EventPayload, EventStatus, EventStore},
pending_work::PendingWorkQueue,
plan::{PlanStatus, PlanStep},
sleep_status::SleepStatusSnapshot,
};
use super::{
DashboardContextCompositionSnapshot, DashboardPendingUserInput, DashboardPlanStep,
DashboardRuntimeActivity, DashboardRuntimeActivityStatus, DashboardRuntimeOptimizationSnapshot,
DashboardRuntimeStatusLevel, DashboardSkillOptimizationSnapshot, DashboardState,
DashboardStatusCommandSnapshot, DashboardTokenUsageSnapshot, DashboardWorkflowInputField,
DashboardWorkflowLoadError, DashboardWorkflowSummary, activity_events_from_history_items,
dashboard_agent_name, render_activity_from_messages, sync_dashboard_runtime_status_live_cell,
};
pub const AUTO_SLEEP_IDLE_THRESHOLD: Duration = Duration::from_mins(5);
pub const AUTO_SLEEP_MIN_INTERVAL: Duration = Duration::from_mins(5);
pub const FORCE_SLEEP_ERROR_BACKLOG_THRESHOLD: usize = 128;
pub fn sync_dashboard_state(
context: &Context,
tx: &tokio::sync::watch::Sender<DashboardState>,
sleep_status: &SleepStatusSnapshot,
last_cycle_elapsed_ms: Option<u128>,
) {
tx.send_modify(|state| {
let app_renders = context.apps.state_renders();
state.agent_name = dashboard_agent_name();
state.session_title = context.session_title.snapshot();
state.status_output = render_status_command_output_for_dashboard(context, &app_renders);
state.status_command = status_command_snapshot_for_dashboard(context);
state.sleep_status_output = render_sleep_status_output_for_dashboard(context, sleep_status);
state.inspect_telegram_output = render_telegram_status_for_dashboard(context);
state.system_prompt_output = render_system_prompt_output_for_dashboard(context);
state.app_status_outputs = render_app_status_outputs_for_dashboard(context);
state.skills = context.openskills.dashboard_summaries();
state.skill_errors = context.openskills.dashboard_errors();
state.workflows = dashboard_workflow_summaries(context);
state.workflow_errors = dashboard_workflow_errors(context);
state.pending_access_requests = context.telegram_acl.pending_requests();
state.pending_user_inputs = pending_user_inputs_for_dashboard(context);
state.activity_events = if state.activity_history.items.is_empty() {
render_activity_for_dashboard(context)
} else {
activity_events_from_history_items(&state.activity_history.items)
};
state.last_cycle_elapsed_ms = last_cycle_elapsed_ms.map(duration_millis_to_u64);
state.runtime_activity = runtime_activity_for_dashboard(
context,
sleep_status,
state.runtime_status.as_deref(),
state.runtime_status_level,
);
sync_dashboard_runtime_status_live_cell(state);
state.footer_context =
render_dashboard_footer_context(context, state.footer_estimated_input_tokens);
state.current_plan_step = current_plan_step_for_dashboard(context);
state.token_usage = token_usage_snapshot_for_dashboard(context);
state.runtime_optimization = runtime_optimization_snapshot_for_dashboard(sleep_status);
state.skill_optimization = skill_optimization_snapshot_for_dashboard(sleep_status);
state.context_composition = context_composition_snapshot_for_dashboard(context);
});
}
pub fn dashboard_workflow_summaries(context: &Context) -> Vec<DashboardWorkflowSummary> {
context
.workflows
.definitions()
.map(|workflow| DashboardWorkflowSummary {
id: workflow.id.clone(),
path: workflow.path.display().to_string(),
input_schema: workflow.input_schema.clone(),
output_schema: workflow.output_schema.clone(),
input_fields: workflow_input_fields(&workflow.input_schema),
})
.collect()
}
pub fn dashboard_workflow_errors(context: &Context) -> Vec<DashboardWorkflowLoadError> {
context
.workflows
.errors()
.iter()
.map(|error| DashboardWorkflowLoadError {
path: error.path.display().to_string(),
message: error.message.clone(),
})
.collect()
}
fn workflow_input_fields(schema: &serde_json::Value) -> Vec<DashboardWorkflowInputField> {
let mut fields = schema
.get("properties")
.and_then(serde_json::Value::as_object)
.map(|properties| {
properties
.iter()
.map(|(name, schema)| DashboardWorkflowInputField {
name: name.clone(),
schema: schema.clone(),
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
fields.sort_by(|left, right| left.name.cmp(&right.name));
fields
}
pub fn current_plan_step_for_dashboard(context: &Context) -> Option<DashboardPlanStep> {
let step = context
.plan
.steps()
.iter()
.find(|step| matches!(step.status, PlanStatus::InProgress))
.or_else(|| {
context
.plan
.steps()
.iter()
.find(|step| matches!(step.status, PlanStatus::Pending))
})?;
Some(DashboardPlanStep {
status: dashboard_plan_status(step),
step: step.step.clone(),
})
}
fn dashboard_plan_status(step: &PlanStep) -> String {
match step.status {
PlanStatus::Pending => "pending",
PlanStatus::InProgress => "in_progress",
PlanStatus::Completed => "completed",
}
.to_string()
}
pub fn token_usage_snapshot_for_dashboard(context: &Context) -> DashboardTokenUsageSnapshot {
DashboardTokenUsageSnapshot {
main: visible_token_usage(Some(context.model_provider.token_usage_info())),
main_model: Some(context.model_provider.model_name()),
judge: visible_token_usage(Some(context.efficient_model_provider.token_usage_info())),
judge_model: Some(context.efficient_model_provider.model_name()),
efficient_model: Some(context.config.efficient_model_config().model_id.clone()),
}
}
fn visible_token_usage(info: Option<TokenUsageInfo>) -> Option<TokenUsageInfo> {
info.filter(|info| {
!info.total_token_usage.is_zero()
|| !info.last_token_usage.is_zero()
|| !info.daily_token_usage.is_empty()
})
}
pub fn context_composition_snapshot_for_dashboard(
context: &Context,
) -> Option<DashboardContextCompositionSnapshot> {
context.latest_context_composition.clone()
}
pub fn runtime_optimization_snapshot_for_dashboard(
sleep_status: &SleepStatusSnapshot,
) -> DashboardRuntimeOptimizationSnapshot {
DashboardRuntimeOptimizationSnapshot {
running: sleep_status.running,
current_trigger: sleep_status.current_trigger.map(str::to_string),
last_result: sleep_status.last_result.clone(),
last_completed_at_ms: sleep_status.last_completed_at_ms,
unread_runtime_error_backlog: sleep_status.unread_runtime_error_backlog,
total_runtime_error_cases_consumed: sleep_status.total_runtime_error_cases_consumed,
total_runtime_error_cases: sleep_status.total_runtime_error_cases,
total_runtime_error_reflections: sleep_status.total_runtime_error_reflections,
total_runtime_contract_candidates: sleep_status.total_runtime_contract_candidates,
total_runtime_contract_candidate_evaluations: sleep_status
.total_runtime_contract_candidate_evaluations,
total_runtime_contract_system_additions: sleep_status
.total_runtime_contract_system_additions,
total_runtime_contract_updates: sleep_status.total_runtime_contract_updates,
}
}
pub fn skill_optimization_snapshot_for_dashboard(
sleep_status: &SleepStatusSnapshot,
) -> DashboardSkillOptimizationSnapshot {
DashboardSkillOptimizationSnapshot {
running: sleep_status.running,
current_trigger: sleep_status.current_trigger.map(str::to_string),
last_result: sleep_status.last_result.clone(),
last_completed_at_ms: sleep_status.last_completed_at_ms,
skill_evidence_records: sleep_status.skill_evidence_records,
total_skill_evidence_run_records: sleep_status.total_skill_evidence_run_records,
total_skill_patch_applied: sleep_status.total_skill_patch_applied,
}
}
pub fn render_dashboard_footer_context(
context: &Context,
estimated_input_tokens: Option<usize>,
) -> String {
let model = context.model_provider.model_name();
let effective_window = context
.model_provider
.request_budget_limits()
.context_window_tokens
.max(1);
let worked_prefix = context
.runtime_turn_started_at
.map_or_else(String::new, |started_at| {
let elapsed = started_at.elapsed().as_secs();
format!("Worked for {} · ", format_elapsed_seconds_compact(elapsed))
});
let info = context.model_provider.token_usage_info();
let used = usize::try_from(info.last_token_usage.input_tokens.max(0)).unwrap_or(0);
let calibrated = estimated_input_tokens.map(|est| {
context
.token_estimate_baseline
.calibrated_total_input_tokens(est)
});
let footer_usage = if used > 0 {
Some((used, false))
} else {
calibrated
.or(estimated_input_tokens)
.map(|value| (value, true))
};
let usage_part = match footer_usage {
Some((used, estimated)) => format!(
"{model} · {}{}/{} used",
if estimated { "~" } else { "" },
format_compact_tokens(used),
format_compact_tokens(effective_window)
),
None => format!(
"{model} · {} window",
format_compact_tokens(effective_window)
),
};
if worked_prefix.is_empty() {
usage_part
} else {
format!("{worked_prefix}{usage_part}")
}
}
pub fn render_system_prompt_output_for_dashboard(context: &Context) -> String {
crate::reasoning::prompt_assembler::runtime_system_prompt_text(context)
}
pub fn render_app_status_outputs_for_dashboard(context: &Context) -> Vec<(String, String)> {
context
.apps
.state_renders()
.into_iter()
.map(|(app_id, state)| {
let mut lines = Vec::new();
let key = app_id.to_string().to_ascii_lowercase();
lines.push(format!("App Status: {}", state.title));
lines.push(String::new());
lines.push("[structured_state]".to_string());
lines.push(format!("app_id={key}"));
lines.push(format!("title={}", state.title));
lines.extend(state.lines.iter().cloned());
(key, lines.join("\n"))
})
.collect()
}
fn format_compact_tokens(tokens: usize) -> String {
if tokens >= 1_000_000 {
let major = tokens / 1_000_000;
let minor = (tokens % 1_000_000) / 100_000;
if minor == 0 {
format!("{major}m")
} else {
format!("{major}.{minor}m")
}
} else if tokens >= 1_000 {
let major = tokens / 1_000;
let minor = (tokens % 1_000) / 100;
if minor == 0 {
format!("{major}k")
} else {
format!("{major}.{minor}k")
}
} else {
tokens.to_string()
}
}
fn format_elapsed_seconds_compact(elapsed_seconds: u64) -> String {
if elapsed_seconds < 60 {
return format!("{elapsed_seconds}s");
}
if elapsed_seconds < 3600 {
let minutes = elapsed_seconds / 60;
let seconds = elapsed_seconds % 60;
return format!("{minutes}m {seconds:02}s");
}
let hours = elapsed_seconds / 3600;
let minutes = (elapsed_seconds % 3600) / 60;
let seconds = elapsed_seconds % 60;
format!("{hours}h {minutes:02}m {seconds:02}s")
}
pub fn render_sleep_status_output_for_dashboard(
context: &Context,
sleep_status: &SleepStatusSnapshot,
) -> String {
let mut sections = Vec::new();
let state = if sleep_status.running {
"running"
} else {
"idle"
};
let mut overview_lines = vec![format!("State: {state}")];
if let Some(trigger) = sleep_status.current_trigger {
overview_lines.push(format!("Trigger: {trigger}"));
}
if let Some(last_result) = sleep_status.last_result.as_deref() {
overview_lines.push(format!("Last result: {last_result}"));
}
sections.push(format!("Overview\n{}", overview_lines.join("\n")));
let queue_lines = [
format!(
"• Runtime error queue: {}",
sleep_status.unread_runtime_error_backlog
),
format!(
"• Skill evidence records: {}",
sleep_status.skill_evidence_records
),
];
sections.push(format!("Queues\n{}", queue_lines.join("\n")));
let runtime_error_lines = [
format!("• Total runs: {}", sleep_status.total_runs),
format!(
"• Total consumed error cases: {}",
sleep_status.total_runtime_error_cases_consumed
),
format!(
"• Total runtime error cases: {}",
sleep_status.total_runtime_error_cases
),
format!(
"• Total runtime error reflections: {}",
sleep_status.total_runtime_error_reflections
),
format!(
"• Total runtime contract candidates: {}",
sleep_status.total_runtime_contract_candidates
),
format!(
"• Total runtime contract candidate evaluations: {}",
sleep_status.total_runtime_contract_candidate_evaluations
),
format!(
"• Total runtime contract additions: {}",
sleep_status.total_runtime_contract_system_additions
),
format!(
"• Total runtime contract updates: {}",
sleep_status.total_runtime_contract_updates
),
];
sections.push(format!(
"Runtime Error Correction Totals\n{}",
runtime_error_lines.join("\n")
));
let skill_lines = [
format!(
"• Total skill evidence run records: {}",
sleep_status.total_skill_evidence_run_records
),
format!(
"• Total skill patch applied: {}",
sleep_status.total_skill_patch_applied
),
];
sections.push(format!(
"Skill Optimization Totals\n{}",
skill_lines.join("\n")
));
let mut trigger_lines = vec![
format!(
"• Force backlog threshold: {} runtime errors",
FORCE_SLEEP_ERROR_BACKLOG_THRESHOLD
),
format!(
"• Current runtime error queue: {}",
sleep_status.unread_runtime_error_backlog
),
format!(
"• Auto sleep after idle: {}",
format_duration(AUTO_SLEEP_IDLE_THRESHOLD)
),
format!(
"• Minimum idle sleep interval: {}",
format_duration(AUTO_SLEEP_MIN_INTERVAL)
),
];
match context.idle_since {
Some(idle_since) => trigger_lines.push(format!(
"• Currently idle for {}",
format_duration(idle_since.elapsed())
)),
None => trigger_lines.push("• Currently not idle".to_string()),
}
if let Some(last_idle_sleep_at) = context.last_idle_sleep_at {
trigger_lines.push(format!(
"• Last idle sleep: {} ago",
format_duration(last_idle_sleep_at.elapsed())
));
}
sections.push(format!("Triggers\n{}", trigger_lines.join("\n")));
sections.join("\n\n")
}
fn format_duration(duration: Duration) -> String {
let seconds = duration.as_secs();
if seconds >= 3600 {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
if minutes == 0 {
format!("{hours}h")
} else {
format!("{hours}h {minutes}m")
}
} else if seconds >= 60 {
let minutes = seconds / 60;
let rem = seconds % 60;
if rem == 0 {
format!("{minutes}m")
} else {
format!("{minutes}m {rem}s")
}
} else {
format!("{seconds}s")
}
}
fn duration_millis_to_u64(ms: u128) -> u64 {
u64::try_from(ms).unwrap_or(u64::MAX)
}
pub fn runtime_activity_for_dashboard(
context: &Context,
sleep_status: &SleepStatusSnapshot,
runtime_status: Option<&str>,
runtime_status_level: Option<DashboardRuntimeStatusLevel>,
) -> DashboardRuntimeActivity {
if runtime_status_level == Some(DashboardRuntimeStatusLevel::Error) {
return DashboardRuntimeActivity::new(
DashboardRuntimeActivityStatus::Error,
"Error",
runtime_status.map(str::to_string),
);
}
let active_runtime_phase = context
.active_runtime_phase
.map(|phase| phase.label().to_string());
if context.active_runtime_turn {
let status = match context.active_runtime_phase {
Some(
crate::context::RuntimeTurnPhase::PreflightPreTurnContext
| crate::context::RuntimeTurnPhase::PreflightCompaction
| crate::context::RuntimeTurnPhase::ModelRequest,
) => DashboardRuntimeActivityStatus::Thinking,
Some(crate::context::RuntimeTurnPhase::ToolExecution) => {
DashboardRuntimeActivityStatus::Tooling
}
None => DashboardRuntimeActivityStatus::Running,
};
let label = match status {
DashboardRuntimeActivityStatus::Thinking => "Thinking",
DashboardRuntimeActivityStatus::Tooling => "Using tools",
_ => "Running",
};
return DashboardRuntimeActivity::new(status, label, None)
.with_runtime_turn(active_runtime_phase, context.runtime_turn_started_at_ms);
}
if sleep_status.running {
return DashboardRuntimeActivity::new(
DashboardRuntimeActivityStatus::Waiting,
"Waiting",
runtime_status
.or(Some("Sleep is running"))
.map(str::to_string),
);
}
match runtime_status_level {
Some(
DashboardRuntimeStatusLevel::Debug
| DashboardRuntimeStatusLevel::Info
| DashboardRuntimeStatusLevel::Warn,
) => DashboardRuntimeActivity::new(
DashboardRuntimeActivityStatus::Running,
"Running",
runtime_status.and_then(trimmed_runtime_status_detail),
),
_ => DashboardRuntimeActivity::default(),
}
}
fn trimmed_runtime_status_detail(status: &str) -> Option<String> {
let status = status.trim();
(!status.is_empty()).then(|| status.to_string())
}
pub fn status_command_snapshot_for_dashboard(context: &Context) -> DashboardStatusCommandSnapshot {
let active_plans = context.plan.active_steps().count();
let events = render_status_event_summary(context);
let runtime_turn = status_command_runtime_turn(context);
let plan_steps = context
.plan
.steps()
.iter()
.take(6)
.map(|step| DashboardPlanStep {
status: step.status.to_string(),
step: step.step.clone(),
})
.collect();
DashboardStatusCommandSnapshot {
runtime_turn,
active_plans,
events,
plan_steps,
}
}
fn status_command_runtime_turn(context: &Context) -> String {
if context.active_runtime_turn {
return context.active_runtime_phase.map_or_else(
|| "running".to_string(),
|phase| format!("running ({})", phase.label()),
);
}
"idle".to_string()
}
pub fn render_status_command_output_for_dashboard(
context: &Context,
_: &[(crate::app::AppId, crate::app::AppStateRender)],
) -> String {
let mut sections = Vec::new();
let status = status_command_snapshot_for_dashboard(context);
sections.push(format!(
"Overview\nRuntime turn: {}\nPlans: {}\nEvents: {}",
status.runtime_turn, status.active_plans, status.events
));
let usage_lines = render_status_usage_lines(context);
sections.push(format!("Model usage\n{}", usage_lines.join("\n")));
let plan_lines = render_status_plan_lines(&status);
sections.push(format!("Plan\n{}", plan_lines.join("\n")));
sections.join("\n\n")
}
fn render_status_event_summary(context: &Context) -> String {
render_status_event_summary_from_statuses(
context
.events
.driver_event_statuses()
.into_iter()
.map(|(_, status)| status),
)
}
fn render_status_event_summary_from_statuses(
statuses: impl IntoIterator<Item = EventStatus>,
) -> String {
let mut pending = 0usize;
let mut claimed = 0usize;
let mut awaiting_delivery = 0usize;
let mut failed = 0usize;
for status in statuses {
match status {
EventStatus::Pending => pending += 1,
EventStatus::Claimed => claimed += 1,
EventStatus::AwaitingDelivery => awaiting_delivery += 1,
EventStatus::Failed => failed += 1,
EventStatus::Resolved | EventStatus::Dismissed => {}
}
}
let active = pending + claimed + awaiting_delivery + failed;
if active == 0 {
return "0".to_string();
}
let mut parts = Vec::new();
if pending > 0 {
parts.push(format!("pending={pending}"));
}
if claimed > 0 {
parts.push(format!("claimed={claimed}"));
}
if awaiting_delivery > 0 {
parts.push(format!("awaiting_delivery={awaiting_delivery}"));
}
if failed > 0 {
parts.push(format!("failed={failed}"));
}
format!("{active} active ({})", parts.join(", "))
}
fn render_status_usage_lines(context: &Context) -> Vec<String> {
fn fmt_num(n: i64) -> String {
let s = n.to_string();
let mut r = String::with_capacity(s.len() + s.len() / 3);
for (i, c) in s.chars().enumerate() {
if i > 0 && (s.len() - i).is_multiple_of(3) {
r.push(',');
}
r.push(c);
}
r
}
let mut lines = Vec::new();
for (label, model_provider) in [
("main", context.model_provider.as_ref()),
("efficient", context.efficient_model_provider.as_ref()),
] {
let info = model_provider.token_usage_info();
if info.total_token_usage.is_zero() {
continue;
}
let model = model_provider.model_name();
lines.push(format!(" {label} · {model}"));
if let Some(window) = info.model_context_window {
let used = info.last_token_usage.input_tokens.max(0);
lines.push(render_status_context_usage_line(used, window));
}
let last = &info.last_token_usage;
if !last.is_zero() {
let mut last_parts = vec![format!("{} in", fmt_num(last.input_tokens))];
last_parts.push(format!("{} out", fmt_num(last.output_tokens)));
if last.reasoning_output_tokens > 0 {
last_parts.push(format!(
"{} reasoning",
fmt_num(last.reasoning_output_tokens)
));
}
lines.push(format!(" Last turn: {}", last_parts.join(" · ")));
}
let total = &info.total_token_usage;
let cached_pct = if total.input_tokens > 0 {
let cached = total.cached_input_tokens.max(0);
let input = total.input_tokens.max(1);
let percent = cached.saturating_mul(100).saturating_add(input / 2) / input;
format!("{percent}% cached")
} else {
String::new()
};
if cached_pct.is_empty() {
lines.push(format!(
" Total: {} Used",
format_compact_tokens(
usize::try_from(total.total_tokens.max(0)).unwrap_or(usize::MAX),
),
));
} else {
lines.push(format!(
" Total: {} Used · {}",
format_compact_tokens(
usize::try_from(total.total_tokens.max(0)).unwrap_or(usize::MAX),
),
cached_pct,
));
}
lines.push(String::new()); }
if lines.is_empty() {
return vec!["No token usage recorded yet.".to_string()];
}
while lines.last().is_some_and(std::string::String::is_empty) {
lines.pop();
}
lines
}
fn render_status_context_usage_line(used: i64, window: i64) -> String {
format!(
" Context: {} of {}",
context_bar(used, window),
format_compact_tokens(usize::try_from(window.max(0)).unwrap_or(usize::MAX)),
)
}
fn context_bar(used: i64, window: i64) -> String {
const WIDTH: usize = 20;
if window <= 0 {
return String::new();
}
let used = used.max(0);
let filled_numerator = used
.saturating_mul(i64::try_from(WIDTH).expect("context bar width fits in i64"))
.saturating_add(window / 2);
let filled = usize::try_from(filled_numerator / window)
.unwrap_or(usize::MAX)
.min(WIDTH);
let bar: String = (0..filled)
.map(|_| '\u{2588}')
.chain((0..WIDTH.saturating_sub(filled)).map(|_| '\u{2591}'))
.collect();
let tenths_percent = used.saturating_mul(1_000).saturating_add(window / 2) / window;
let whole_percent = tenths_percent / 10;
let fractional_percent = tenths_percent % 10;
if used > window {
format!("{bar}> {whole_percent}.{fractional_percent}%")
} else {
format!("{bar} {whole_percent}.{fractional_percent}%")
}
}
fn render_status_plan_lines(status: &DashboardStatusCommandSnapshot) -> Vec<String> {
if status.plan_steps.is_empty() {
return vec!["No active plan items.".to_string()];
}
status
.plan_steps
.iter()
.map(|step| format!("• {} [{}]", step.step, step.status))
.collect()
}
pub fn render_telegram_status_for_dashboard(context: &Context) -> String {
let chats = context.telegram.chat_summaries_view();
let pending_requests = context.telegram_acl.pending_requests();
let queued_outbound = chats
.iter()
.map(|chat| chat.pending_outbound_count)
.sum::<usize>();
let mut lines = vec![
"Telegram".to_string(),
"Role: transport / adapter".to_string(),
format!("Known chats: {}", chats.len()),
format!("Pending approvals: {}", pending_requests.len()),
format!("Queued outbound: {queued_outbound}"),
];
if chats.is_empty() && pending_requests.is_empty() {
lines.push(String::new());
lines.push("No chats or pending approvals.".to_string());
return lines.join("\n");
}
if !pending_requests.is_empty() {
lines.push(String::new());
lines.push("Pending approval requests".to_string());
lines.extend(
pending_requests
.iter()
.take(8)
.enumerate()
.map(|(index, request)| {
format!(
"{}. {} ({}) from {} :: {}",
index + 1,
request.title,
request.chat_id,
request.sender,
request.last_message_preview
)
}),
);
}
if !chats.is_empty() {
lines.push(String::new());
lines.push("Chats".to_string());
lines.extend(chats.iter().take(8).map(|chat| {
let mut flags = Vec::new();
if chat.pending_outbound_count > 0 {
flags.push(format!("{} queued", chat.pending_outbound_count));
}
let suffix = if flags.is_empty() {
String::new()
} else {
format!(" [{}]", flags.join(", "))
};
format!("• {} ({}){}", chat.title, chat.chat_id, suffix)
}));
}
lines.join("\n")
}
pub fn pending_user_inputs_for_dashboard(context: &Context) -> Vec<DashboardPendingUserInput> {
pending_user_inputs_from_sources(&context.events, &context.pending_work)
}
pub fn pending_user_inputs_from_sources(
events: &EventStore,
pending_work: &PendingWorkQueue,
) -> Vec<DashboardPendingUserInput> {
pending_work
.pending_event_ids()
.into_iter()
.filter_map(|event_id| {
let event = events.view(&event_id.to_string()).ok()?;
if !matches!(event.status, EventStatus::Pending) {
return None;
}
let EventPayload::TerminalIncoming(payload) = event.payload else {
return None;
};
Some(DashboardPendingUserInput {
event_id: event.event_id.to_string(),
origin: payload.origin,
incoming_text: payload.incoming_text,
arrived_at_ms: event.arrived_at_ms,
attachment_count: payload.attachments.len(),
})
})
.collect()
}
pub fn render_activity_for_dashboard(
context: &Context,
) -> Vec<crate::dashboard::SessionActivityEvent> {
render_activity_from_messages(context.memory.runtime_conversation_messages())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_summary_counts_claimed_events_as_active() {
assert_eq!(
render_status_event_summary_from_statuses([EventStatus::Claimed]),
"1 active (claimed=1)"
);
}
#[test]
fn event_summary_reports_active_event_states() {
assert_eq!(
render_status_event_summary_from_statuses([
EventStatus::Pending,
EventStatus::Claimed,
EventStatus::AwaitingDelivery,
EventStatus::Failed,
EventStatus::Resolved,
EventStatus::Dismissed,
]),
"4 active (pending=1, claimed=1, awaiting_delivery=1, failed=1)"
);
}
#[test]
fn event_summary_ignores_terminal_success_states() {
assert_eq!(
render_status_event_summary_from_statuses([
EventStatus::Resolved,
EventStatus::Dismissed,
]),
"0"
);
}
#[test]
fn status_context_usage_line_shows_one_percentage() {
let line = render_status_context_usage_line(6_206, 272_000);
assert_eq!(line.matches("2.3%").count(), 1);
assert_eq!(
line,
" Context: \u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591}\u{2591} 2.3% of 272k"
);
}
}