use std::collections::HashMap;
use std::time::Duration;
use crate::session::{ClaudeSession, RawSession, SessionStatus, TelemetryStatus, ToolStats};
const PROJECTS: &[(&str, &str, &str)] = &[
("acme-api", "/Users/dev/projects/acme-api", "opus-4.6"),
("acme-api", "/Users/dev/projects/acme-api", "opus-4.6"),
(
"web-frontend",
"/Users/dev/projects/web-frontend",
"sonnet-4.6",
),
("ml-pipeline", "/Users/dev/projects/ml-pipeline", "opus-4.6"),
(
"ml-pipeline",
"/Users/dev/worktrees/ml-pipeline-feat",
"sonnet-4.6",
),
(
"infra-terraform",
"/Users/dev/projects/infra-terraform",
"haiku",
),
("docs-site", "/Users/dev/projects/docs-site", "sonnet-4.6"),
("mobile-app", "/Users/dev/projects/mobile-app", "opus-4.6"),
];
const STATUS_SEQUENCES: &[&[SessionStatus]] = &[
&[
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::NeedsInput,
SessionStatus::NeedsInput,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
],
&[
SessionStatus::Processing,
SessionStatus::NeedsInput,
SessionStatus::NeedsInput,
SessionStatus::NeedsInput,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
SessionStatus::Processing,
],
&[
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
SessionStatus::WaitingInput,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::NeedsInput,
SessionStatus::Processing,
],
&[
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
SessionStatus::Processing,
SessionStatus::Processing,
],
&[
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::NeedsInput,
],
&[
SessionStatus::WaitingInput,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::NeedsInput,
SessionStatus::Processing,
],
&[
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
SessionStatus::WaitingInput,
],
&[
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::NeedsInput,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::Processing,
SessionStatus::WaitingInput,
SessionStatus::Processing,
],
];
const PENDING_TOOLS: &[(&str, &str)] = &[
("Bash", "cargo test --workspace"),
("Bash", "cargo clippy -- -D warnings"),
("Bash", "npm run build && npm test"),
("Bash", "python train.py --epochs 50"),
("Bash", "rm -rf /tmp/cache && rm -rf node_modules"),
("Bash", "terraform apply -auto-approve"),
("Bash", "npm run deploy -- --prod"),
("Bash", "git push --force origin main"),
];
pub fn generate_sessions(tick: u32) -> Vec<ClaudeSession> {
let base_pid = 10000u32;
PROJECTS
.iter()
.enumerate()
.map(|(i, (name, cwd, model))| {
let pid = base_pid + (i as u32 * 1111);
let raw = RawSession {
pid,
session_id: format!("demo-{:04x}-{:04x}-{:04x}", i, i * 7, i * 13),
cwd: cwd.to_string(),
started_at: 0, };
let mut s = ClaudeSession::from_raw(raw);
s.project_name = name.to_string();
s.model = model.to_string();
s.telemetry_status = TelemetryStatus::Available;
s.usage_metrics_available = true;
let seq = STATUS_SEQUENCES[i % STATUS_SEQUENCES.len()];
s.status = seq[(tick as usize) % seq.len()];
let base_tokens = (i as u64 + 1) * 50_000 + (tick as u64) * 2_000;
s.total_input_tokens = base_tokens;
s.total_output_tokens = base_tokens / 5;
s.cache_read_tokens = base_tokens / 3;
s.cache_write_tokens = base_tokens / 10;
let ctx_rate = 0.3 + (i as f64 * 0.08);
let ctx_pct = ((tick as f64 * ctx_rate) % 95.0) + 5.0;
s.context_max = crate::monitor::model_context_max(model);
s.context_tokens = (s.context_max as f64 * ctx_pct / 100.0) as u64;
s.cost_usd = (i as f64 + 1.0) * 0.15 + (tick as f64) * 0.03 * (i as f64 + 1.0);
s.burn_rate_per_hr = if matches!(s.status, SessionStatus::Processing) {
2.0 + (i as f64 * 0.8)
} else {
0.0
};
let base_elapsed = (i as u64 + 1) * 300 + tick as u64 * 2;
s.elapsed = Duration::from_secs(base_elapsed);
s.cpu_percent = match s.status {
SessionStatus::Processing => 15.0 + (i as f32 * 3.0),
SessionStatus::NeedsInput => 0.3,
_ => 0.8,
};
s.mem_mb = 200.0 + (i as f64 * 50.0);
if i == 0 || i == 3 {
s.subagent_count = 2 + (tick as usize % 3);
}
for t in 0..15 {
let past_tick = if tick > 15 { tick - 15 + t } else { t };
let past_status = seq[(past_tick as usize) % seq.len()];
let level = match past_status {
SessionStatus::Processing => 7,
SessionStatus::NeedsInput => 4,
SessionStatus::WaitingInput => 2,
SessionStatus::Unknown => 2,
SessionStatus::Idle => 1,
SessionStatus::Finished => 0,
};
s.activity_history.push(level);
}
s.worktree_id = Some(cwd.to_string());
if tick > 3 {
let mut tools = HashMap::new();
tools.insert(
"Bash".to_string(),
ToolStats {
calls: 12 + (i as u32 * 3),
},
);
tools.insert(
"Read".to_string(),
ToolStats {
calls: 25 + (i as u32 * 5),
},
);
tools.insert(
"Edit".to_string(),
ToolStats {
calls: 8 + (i as u32 * 2),
},
);
tools.insert(
"Grep".to_string(),
ToolStats {
calls: 6 + (i as u32),
},
);
s.tool_usage = tools;
}
if tick > 5 {
let mut files = HashMap::new();
files.insert(format!("/Users/dev/projects/{name}/src/main.rs"), 3);
files.insert(format!("/Users/dev/projects/{name}/src/lib.rs"), 1);
if i % 2 == 0 {
files.insert(format!("/Users/dev/projects/{name}/Cargo.toml"), 1);
}
s.files_modified = files;
}
if i == 2 {
s.total_input_tokens = 120_000 + (tick as u64 * 3_000);
s.cache_read_tokens = 5_000; s.cache_write_tokens = 2_000;
}
if i == 3 && tick > 6 {
let saturation = 0.91 + ((tick as f64 - 6.0) * 0.005).min(0.07);
s.context_tokens = (s.context_max as f64 * saturation) as u64;
}
if i == 5 {
s.cost_usd = 7.50 + (tick as f64) * 0.12;
s.elapsed = Duration::from_secs(900 + tick as u64 * 5);
s.files_modified.clear(); }
if i == 7 && tick > 4 {
s.cost_usd = 3.0 + (tick as f64) * 0.05;
let elapsed_hrs = s.elapsed.as_secs_f64() / 3600.0;
let avg_rate = if elapsed_hrs > 0.01 {
s.cost_usd / elapsed_hrs
} else {
1.0
};
s.burn_rate_per_hr = avg_rate * 6.0;
}
if i == 4 && tick > 5 {
s.last_tool_error = true;
s.tool_usage
.entry("Bash".to_string())
.and_modify(|t| t.calls = 15 + (tick % 5))
.or_insert(ToolStats {
calls: 15 + (tick % 5),
});
}
if s.status == SessionStatus::NeedsInput {
let (tool, cmd) = PENDING_TOOLS[i % PENDING_TOOLS.len()];
s.pending_tool_name = Some(tool.to_string());
s.pending_tool_input = Some(cmd.to_string());
}
s
})
.collect()
}
pub fn demo_event(tick: u32) -> Option<DemoEvent> {
const CYCLE_LEN: u32 = 24;
let phase = tick % CYCLE_LEN;
match phase {
3 => Some(DemoEvent {
message: "Rule 'approve-cargo': approved acme-api (Bash: cargo test --workspace)"
.into(),
kind: EventKind::RuleAction,
}),
5 => Some(DemoEvent {
message:
"Rule 'deny-rm-rf': denied ml-pipeline (Bash: rm -rf /tmp/cache && rm -rf node_modules)"
.into(),
kind: EventKind::RuleAction,
}),
8 => Some(DemoEvent {
message: "Rule 'approve-cargo': approved acme-api (Bash: cargo clippy -- -D warnings)"
.into(),
kind: EventKind::RuleAction,
}),
10 => Some(DemoEvent {
message: "Brain: approve Bash(npm run build) for web-frontend — safe build command"
.into(),
kind: EventKind::BrainSuggestion,
}),
13 => Some(DemoEvent {
message: "Brain: deny Bash(terraform apply -auto-approve) — destructive without plan review".into(),
kind: EventKind::BrainSuggestion,
}),
15 => Some(DemoEvent {
message:
"Brain suggested approve, but deny rule 'deny-force-push' overrides (git push --force)"
.into(),
kind: EventKind::BrainOverride,
}),
18 => Some(DemoEvent {
message: "Routed summary from ml-pipeline → docs-site: \"Added training pipeline with checkpoint support\"".into(),
kind: EventKind::Route,
}),
20 => Some(DemoEvent {
message: "Health: infra-terraform stalled — $8.40 spent, 16 min, no file edits".into(),
kind: EventKind::HealthAlert,
}),
22 => Some(DemoEvent {
message: "Health: ml-pipeline context at 94% — consider spawning fresh session".into(),
kind: EventKind::HealthAlert,
}),
_ => None,
}
}
pub struct DemoEvent {
pub message: String,
pub kind: EventKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventKind {
RuleAction,
BrainSuggestion,
BrainOverride,
Route,
HealthAlert,
}
pub fn demo_rules() -> Vec<crate::rules::AutoRule> {
use crate::rules::{AutoRule, RuleAction};
vec![
{
let mut r = AutoRule::new("approve-cargo".into(), RuleAction::Approve);
r.match_tool = vec!["Bash".into()];
r.match_command = vec!["cargo".into()];
r
},
{
let mut r = AutoRule::new("deny-rm-rf".into(), RuleAction::Deny);
r.match_command = vec!["rm -rf".into()];
r
},
{
let mut r = AutoRule::new("deny-force-push".into(), RuleAction::Deny);
r.match_command = vec!["--force".into()];
r
},
{
let mut r = AutoRule::new("kill-runaway".into(), RuleAction::Terminate);
r.match_cost_above = Some(20.0);
r
},
]
}