1use crate::core::events::{EventKind, LeanCtxEvent};
2use crate::core::gain::gain_score::GainScore;
3use crate::core::gain::model_pricing::ModelPricing;
4use crate::core::gain::task_classifier::{TaskCategory, TaskClassifier};
5use crate::tui::event_reader::EventTail;
6use crossterm::event::{self, Event, KeyCode, KeyEventKind};
7use crossterm::terminal::{
8 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
9};
10use crossterm::ExecutableCommand;
11use ratatui::layout::{Constraint, Direction, Layout, Rect};
12use ratatui::style::{Color, Modifier, Style};
13use ratatui::text::{Line, Span};
14use ratatui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Row, Table};
15use ratatui::Terminal;
16use std::io::stdout;
17use std::time::{Duration, Instant};
18
19const GREEN: Color = Color::Rgb(52, 211, 153);
20const PURPLE: Color = Color::Rgb(129, 140, 248);
21const BLUE: Color = Color::Rgb(56, 189, 248);
22const YELLOW: Color = Color::Rgb(251, 191, 36);
23const RED: Color = Color::Rgb(248, 113, 113);
24const MUTED: Color = Color::Rgb(107, 107, 136);
25const SURFACE: Color = Color::Rgb(10, 10, 18);
26const BG: Color = Color::Rgb(6, 6, 10);
27
28struct AppState {
29 events: Vec<LeanCtxEvent>,
30 total_saved: u64,
31 total_original: u64,
32 cache_hits: u64,
33 total_calls: u64,
34 files: std::collections::HashMap<String, FileHeat>,
35 gain_score: Option<GainScore>,
36 last_gain_refresh: Instant,
37 quit: bool,
38 focus: usize,
39}
40
41struct FileHeat {
42 access_count: u32,
43 tokens_saved: u64,
44}
45
46impl AppState {
47 fn new() -> Self {
48 let store = crate::core::stats::load();
49 let heatmap = crate::core::heatmap::HeatMap::load();
50 let files = heatmap
51 .entries
52 .values()
53 .map(|e| {
54 (
55 e.path.clone(),
56 FileHeat {
57 access_count: e.access_count,
58 tokens_saved: e.total_tokens_saved,
59 },
60 )
61 })
62 .collect();
63 Self {
64 events: Vec::new(),
65 total_saved: store
66 .total_input_tokens
67 .saturating_sub(store.total_output_tokens),
68 total_original: store.total_input_tokens,
69 cache_hits: store.cep.total_cache_hits,
70 total_calls: store.total_commands,
71 files,
72 gain_score: None,
73 last_gain_refresh: Instant::now(),
74 quit: false,
75 focus: 0,
76 }
77 }
78
79 fn ingest(&mut self, new_events: Vec<LeanCtxEvent>) {
80 for ev in &new_events {
81 match &ev.kind {
82 EventKind::ToolCall {
83 tool: _,
84 tokens_original,
85 tokens_saved,
86 path,
87 ..
88 } => {
89 self.total_saved += tokens_saved;
90 self.total_original += tokens_original;
91 self.total_calls += 1;
92 if let Some(p) = path {
93 let entry = self.files.entry(p.clone()).or_insert(FileHeat {
94 access_count: 0,
95 tokens_saved: 0,
96 });
97 entry.access_count += 1;
98 entry.tokens_saved += tokens_saved;
99 }
100 }
101 EventKind::CacheHit { path, saved_tokens } => {
102 self.cache_hits += 1;
103 self.total_saved += saved_tokens;
104 let entry = self.files.entry(path.clone()).or_insert(FileHeat {
105 access_count: 0,
106 tokens_saved: 0,
107 });
108 entry.access_count += 1;
109 entry.tokens_saved += saved_tokens;
110 }
111 EventKind::Compression { path, .. } => {
112 let entry = self.files.entry(path.clone()).or_insert(FileHeat {
113 access_count: 0,
114 tokens_saved: 0,
115 });
116 entry.access_count += 1;
117 }
118 _ => {}
119 }
120 }
121 self.events.extend(new_events);
122 if self.events.len() > 200 {
123 let drain = self.events.len() - 200;
124 self.events.drain(..drain);
125 }
126 }
127
128 fn savings_pct(&self) -> f64 {
129 if self.total_original == 0 {
130 return 0.0;
131 }
132 self.total_saved as f64 / self.total_original as f64 * 100.0
133 }
134
135 fn cache_rate(&self) -> f64 {
136 if self.total_calls == 0 {
137 return 0.0;
138 }
139 self.cache_hits as f64 / self.total_calls as f64 * 100.0
140 }
141
142 fn refresh_gain_score(&mut self) {
143 if self.last_gain_refresh.elapsed() < Duration::from_secs(2) {
144 return;
145 }
146 let engine = crate::core::gain::GainEngine::load();
147 self.gain_score = Some(engine.gain_score(None));
148 self.last_gain_refresh = Instant::now();
149 }
150}
151
152pub fn run() -> anyhow::Result<()> {
153 enable_raw_mode()?;
154 stdout().execute(EnterAlternateScreen)?;
155 let backend = ratatui::backend::CrosstermBackend::new(stdout());
156 let mut terminal = Terminal::new(backend)?;
157
158 let mut state = AppState::new();
159 let mut tail = EventTail::new();
160 let tick_rate = Duration::from_millis(200);
161 let mut last_tick = Instant::now();
162
163 loop {
164 terminal.draw(|f| draw(f, &state))?;
165
166 let timeout = tick_rate.saturating_sub(last_tick.elapsed());
167 if event::poll(timeout)? {
168 if let Event::Key(key) = event::read()? {
169 if key.kind == KeyEventKind::Press {
170 match key.code {
171 KeyCode::Char('q') | KeyCode::Esc => state.quit = true,
172 KeyCode::Tab => state.focus = (state.focus + 1) % 5,
173 KeyCode::Char('1') => state.focus = 0,
174 KeyCode::Char('2') => state.focus = 1,
175 KeyCode::Char('3') => state.focus = 2,
176 KeyCode::Char('4') => state.focus = 3,
177 KeyCode::Char('5') => state.focus = 4,
178 _ => {}
179 }
180 }
181 }
182 }
183
184 if last_tick.elapsed() >= tick_rate {
185 let new = tail.poll();
186 if !new.is_empty() {
187 state.ingest(new);
188 }
189 state.refresh_gain_score();
190 last_tick = Instant::now();
191 }
192
193 if state.quit {
194 break;
195 }
196 }
197
198 disable_raw_mode()?;
199 stdout().execute(LeaveAlternateScreen)?;
200 Ok(())
201}
202
203fn draw(f: &mut ratatui::Frame, state: &AppState) {
204 let size = f.area();
205
206 let header_body = Layout::default()
207 .direction(Direction::Vertical)
208 .constraints([Constraint::Length(3), Constraint::Min(0)])
209 .split(size);
210
211 draw_header(f, header_body[0], state);
212
213 let columns = Layout::default()
214 .direction(Direction::Horizontal)
215 .constraints([Constraint::Percentage(65), Constraint::Percentage(35)])
216 .split(header_body[1]);
217
218 let left = Layout::default()
219 .direction(Direction::Vertical)
220 .constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
221 .split(columns[0]);
222
223 let right = Layout::default()
224 .direction(Direction::Vertical)
225 .constraints([
226 Constraint::Percentage(38),
227 Constraint::Percentage(37),
228 Constraint::Percentage(25),
229 ])
230 .split(columns[1]);
231
232 draw_live_feed(f, left[0], state);
233 draw_heatmap(f, left[1], state);
234 draw_savings(f, right[0], state);
235 draw_session(f, right[1], state);
236 draw_task_activity(f, right[2], state);
237}
238
239fn draw_header(f: &mut ratatui::Frame, area: Rect, state: &AppState) {
240 let saved = format_tokens(state.total_saved);
241 let pct = format!("{:.0}%", state.savings_pct());
242 let env_model = std::env::var("LEAN_CTX_MODEL")
243 .or_else(|_| std::env::var("LCTX_MODEL"))
244 .ok();
245 let pricing = ModelPricing::load();
246 let quote = pricing.quote(env_model.as_deref());
247 let cost = format!(
248 "${:.3}",
249 state.total_saved as f64 * quote.cost.input_per_m / 1_000_000.0
250 );
251 let gain_score = state.gain_score.as_ref().map_or(0, |s| s.total);
252 let trend_icon = state.gain_score.as_ref().map_or("─", |s| match s.trend {
253 crate::core::gain::gain_score::Trend::Rising => "▲",
254 crate::core::gain::gain_score::Trend::Stable => "─",
255 crate::core::gain::gain_score::Trend::Declining => "▼",
256 });
257 let trend_color = state.gain_score.as_ref().map_or(MUTED, |s| match s.trend {
258 crate::core::gain::gain_score::Trend::Rising => GREEN,
259 crate::core::gain::gain_score::Trend::Stable => MUTED,
260 crate::core::gain::gain_score::Trend::Declining => YELLOW,
261 });
262
263 let spans = vec![
264 Span::styled(
265 " LeanCTX ",
266 Style::default().fg(GREEN).add_modifier(Modifier::BOLD),
267 ),
268 Span::styled("Observatory ", Style::default().fg(MUTED)),
269 Span::raw(" "),
270 Span::styled(format!("{saved} saved"), Style::default().fg(GREEN)),
271 Span::raw(" "),
272 Span::styled(format!("{pct} compression"), Style::default().fg(PURPLE)),
273 Span::raw(" "),
274 Span::styled(format!("{cost} avoided"), Style::default().fg(BLUE)),
275 Span::raw(" "),
276 Span::styled(format!("{gain_score}/100 gain"), Style::default().fg(GREEN)),
277 Span::styled(format!(" {trend_icon}"), Style::default().fg(trend_color)),
278 Span::raw(" "),
279 Span::styled(
280 format!("{} events", state.events.len()),
281 Style::default().fg(MUTED),
282 ),
283 ];
284
285 let header = Paragraph::new(Line::from(spans)).block(
286 Block::default()
287 .borders(Borders::BOTTOM)
288 .border_style(Style::default().fg(Color::Rgb(30, 30, 50))),
289 );
290 f.render_widget(header, area);
291}
292
293fn draw_task_activity(f: &mut ratatui::Frame, area: Rect, state: &AppState) {
294 let block = Block::default()
295 .title(Span::styled(
296 " Task Activity ",
297 Style::default().fg(GREEN).add_modifier(Modifier::BOLD),
298 ))
299 .borders(Borders::ALL)
300 .border_style(Style::default().fg(if state.focus == 4 {
301 GREEN
302 } else {
303 Color::Rgb(30, 30, 50)
304 }))
305 .style(Style::default().bg(SURFACE));
306
307 let mut counts: std::collections::HashMap<TaskCategory, u64> = std::collections::HashMap::new();
308 for ev in state.events.iter().rev().take(120) {
309 if let EventKind::ToolCall { tool, .. } = &ev.kind {
310 let cat = TaskClassifier::classify_tool(tool);
311 *counts.entry(cat).or_insert(0) += 1;
312 }
313 }
314
315 let mut rows: Vec<(TaskCategory, u64)> = counts.into_iter().collect();
316 rows.sort_by_key(|x| std::cmp::Reverse(x.1));
317
318 let max_items = area.height.saturating_sub(2) as usize;
319 let items: Vec<ListItem> = if rows.is_empty() {
320 vec![ListItem::new(Line::from(vec![Span::styled(
321 "No tool calls yet.",
322 Style::default().fg(MUTED),
323 )]))]
324 } else {
325 rows.into_iter()
326 .take(max_items)
327 .map(|(cat, n)| {
328 ListItem::new(Line::from(vec![
329 Span::styled(
330 format!("{:<14}", cat.label()),
331 Style::default().fg(Color::Rgb(220, 220, 240)),
332 ),
333 Span::styled(format!("{n:>4}"), Style::default().fg(MUTED)),
334 ]))
335 })
336 .collect()
337 };
338
339 let list = List::new(items).block(block);
340 f.render_widget(list, area);
341}
342
343fn draw_live_feed(f: &mut ratatui::Frame, area: Rect, state: &AppState) {
344 let block = Block::default()
345 .title(Span::styled(
346 " Live Feed ",
347 Style::default().fg(GREEN).add_modifier(Modifier::BOLD),
348 ))
349 .borders(Borders::ALL)
350 .border_style(Style::default().fg(if state.focus == 0 {
351 GREEN
352 } else {
353 Color::Rgb(30, 30, 50)
354 }))
355 .style(Style::default().bg(SURFACE));
356
357 let visible = area.height.saturating_sub(2) as usize;
358 let start = state.events.len().saturating_sub(visible);
359 let items: Vec<ListItem> = state.events[start..]
360 .iter()
361 .rev()
362 .map(|ev| {
363 let (icon, tool, detail, color) = match &ev.kind {
364 EventKind::ToolCall {
365 tool,
366 tokens_original,
367 tokens_saved,
368 mode,
369 ..
370 } => {
371 let pct = if *tokens_original > 0 {
372 format!("-{}%", tokens_saved * 100 / tokens_original)
373 } else {
374 String::new()
375 };
376 let m = mode.as_deref().unwrap_or("");
377 (
378 ">>",
379 tool.as_str(),
380 format!(
381 "{} {}t->{}t {}",
382 m,
383 tokens_original,
384 tokens_original - tokens_saved,
385 pct
386 ),
387 GREEN,
388 )
389 }
390 EventKind::CacheHit { path, saved_tokens } => {
391 let short = path.rsplit('/').next().unwrap_or(path);
392 (
393 "**",
394 "cache",
395 format!("{short} {saved_tokens}t saved"),
396 PURPLE,
397 )
398 }
399 EventKind::Compression {
400 path,
401 strategy,
402 before_lines,
403 after_lines,
404 ..
405 } => {
406 let short = path.rsplit('/').next().unwrap_or(path);
407 (
408 "~~",
409 "compress",
410 format!("{short} {strategy} {before_lines}L->{after_lines}L"),
411 BLUE,
412 )
413 }
414 EventKind::AgentAction {
415 agent_id, action, ..
416 } => ("@@", "agent", format!("{agent_id} {action}"), YELLOW),
417 EventKind::KnowledgeUpdate {
418 category,
419 key,
420 action,
421 } => (
422 "!!",
423 "knowledge",
424 format!("{action} {category}/{key}"),
425 PURPLE,
426 ),
427 EventKind::ThresholdShift {
428 language,
429 new_entropy,
430 new_jaccard,
431 ..
432 } => (
433 "~~",
434 "threshold",
435 format!("{language} e={new_entropy:.2} j={new_jaccard:.2}"),
436 MUTED,
437 ),
438 EventKind::BudgetWarning {
439 role,
440 dimension,
441 percent,
442 ..
443 } => (
444 "$$",
445 "budget",
446 format!("{role} {dimension} {percent}% WARNING"),
447 YELLOW,
448 ),
449 EventKind::BudgetExhausted {
450 role, dimension, ..
451 } => ("!!", "budget", format!("{role} {dimension} EXHAUSTED"), RED),
452 EventKind::PolicyViolation { role, tool, reason } => (
453 "XX",
454 "policy",
455 format!("{role} blocked {tool}: {reason}"),
456 RED,
457 ),
458 EventKind::RoleChanged { from, to } => {
459 ("->", "role", format!("{from} -> {to}"), BLUE)
460 }
461 };
462 let ts = &ev.timestamp[11..19.min(ev.timestamp.len())];
463 ListItem::new(Line::from(vec![
464 Span::styled(format!("{ts} "), Style::default().fg(MUTED)),
465 Span::styled(format!("{icon} "), Style::default().fg(color)),
466 Span::styled(
467 format!("{tool:14}"),
468 Style::default().fg(color).add_modifier(Modifier::BOLD),
469 ),
470 Span::styled(detail, Style::default().fg(MUTED)),
471 ]))
472 })
473 .collect();
474
475 let list = List::new(items).block(block);
476 f.render_widget(list, area);
477}
478
479fn draw_heatmap(f: &mut ratatui::Frame, area: Rect, state: &AppState) {
480 let block = Block::default()
481 .title(Span::styled(
482 " File Heatmap ",
483 Style::default().fg(YELLOW).add_modifier(Modifier::BOLD),
484 ))
485 .borders(Borders::ALL)
486 .border_style(Style::default().fg(if state.focus == 2 {
487 GREEN
488 } else {
489 Color::Rgb(30, 30, 50)
490 }))
491 .style(Style::default().bg(SURFACE));
492
493 let mut files: Vec<_> = state.files.iter().collect();
494 files.sort_by_key(|x| std::cmp::Reverse(x.1.access_count));
495 if files.is_empty() {
496 let msg = Paragraph::new("Waiting for file activity...")
497 .style(Style::default().fg(MUTED))
498 .block(block);
499 f.render_widget(msg, area);
500 return;
501 }
502 let max_access = files.first().map_or(1, |f| f.1.access_count).max(1);
503
504 let visible = (area.height.saturating_sub(2)) as usize;
505 let rows: Vec<Row> = files
506 .iter()
507 .take(visible)
508 .map(|(path, heat)| {
509 let short = path.rsplit('/').next().unwrap_or(path);
510 let bar_len = (heat.access_count as f64 / max_access as f64 * 12.0) as usize;
511 let bar: String = "█".repeat(bar_len) + &"░".repeat(12 - bar_len);
512 Row::new(vec![
513 ratatui::widgets::Cell::from(Span::styled(
514 format!("{short:20}"),
515 Style::default().fg(Color::White),
516 )),
517 ratatui::widgets::Cell::from(Span::styled(bar, Style::default().fg(YELLOW))),
518 ratatui::widgets::Cell::from(Span::styled(
519 format!("{}x", heat.access_count),
520 Style::default().fg(MUTED),
521 )),
522 ratatui::widgets::Cell::from(Span::styled(
523 format!("{}t", format_tokens(heat.tokens_saved)),
524 Style::default().fg(GREEN),
525 )),
526 ])
527 })
528 .collect();
529
530 let table = Table::new(
531 rows,
532 [
533 Constraint::Length(22),
534 Constraint::Length(14),
535 Constraint::Length(6),
536 Constraint::Length(10),
537 ],
538 )
539 .block(block);
540 f.render_widget(table, area);
541}
542
543fn draw_savings(f: &mut ratatui::Frame, area: Rect, state: &AppState) {
544 let block = Block::default()
545 .title(Span::styled(
546 " Token Savings ",
547 Style::default().fg(GREEN).add_modifier(Modifier::BOLD),
548 ))
549 .borders(Borders::ALL)
550 .border_style(Style::default().fg(if state.focus == 1 {
551 GREEN
552 } else {
553 Color::Rgb(30, 30, 50)
554 }))
555 .style(Style::default().bg(SURFACE));
556
557 let inner = block.inner(area);
558 f.render_widget(block, area);
559
560 let chunks = Layout::default()
561 .direction(Direction::Vertical)
562 .constraints([
563 Constraint::Length(2),
564 Constraint::Length(3),
565 Constraint::Length(1),
566 Constraint::Length(2),
567 Constraint::Length(3),
568 Constraint::Min(0),
569 ])
570 .split(inner);
571
572 let pct = state.savings_pct();
573 f.render_widget(
574 Paragraph::new(Line::from(vec![
575 Span::styled(
576 format!(" {} saved ", format_tokens(state.total_saved)),
577 Style::default().fg(GREEN).add_modifier(Modifier::BOLD),
578 ),
579 Span::styled(format!("({pct:.0}%)"), Style::default().fg(MUTED)),
580 ])),
581 chunks[0],
582 );
583
584 let ratio = (pct / 100.0).min(1.0);
585 f.render_widget(
586 Gauge::default()
587 .ratio(ratio)
588 .gauge_style(Style::default().fg(GREEN).bg(BG))
589 .label(format!("{pct:.0}%")),
590 chunks[1],
591 );
592
593 f.render_widget(Paragraph::new(""), chunks[2]);
594
595 let cache_pct = state.cache_rate();
596 f.render_widget(
597 Paragraph::new(Line::from(vec![
598 Span::styled(" Cache Hit Rate ", Style::default().fg(PURPLE)),
599 Span::styled(format!("{cache_pct:.0}%"), Style::default().fg(MUTED)),
600 ])),
601 chunks[3],
602 );
603
604 let cache_ratio = (cache_pct / 100.0).min(1.0);
605 f.render_widget(
606 Gauge::default()
607 .ratio(cache_ratio)
608 .gauge_style(Style::default().fg(PURPLE).bg(BG))
609 .label(format!("{cache_pct:.0}%")),
610 chunks[4],
611 );
612}
613
614fn draw_session(f: &mut ratatui::Frame, area: Rect, state: &AppState) {
615 let block = Block::default()
616 .title(Span::styled(
617 " Session ",
618 Style::default().fg(BLUE).add_modifier(Modifier::BOLD),
619 ))
620 .borders(Borders::ALL)
621 .border_style(Style::default().fg(if state.focus == 3 {
622 GREEN
623 } else {
624 Color::Rgb(30, 30, 50)
625 }))
626 .style(Style::default().bg(SURFACE));
627
628 let cost = state.total_saved as f64 * 2.5 / 1_000_000.0;
629
630 let lines = vec![
631 Line::from(vec![
632 Span::styled(" Calls ", Style::default().fg(MUTED)),
633 Span::styled(
634 format!("{}", state.total_calls),
635 Style::default().fg(Color::White),
636 ),
637 ]),
638 Line::from(vec![
639 Span::styled(" Files ", Style::default().fg(MUTED)),
640 Span::styled(
641 format!("{}", state.files.len()),
642 Style::default().fg(Color::White),
643 ),
644 ]),
645 Line::from(vec![
646 Span::styled(" Original ", Style::default().fg(MUTED)),
647 Span::styled(
648 format_tokens(state.total_original),
649 Style::default().fg(Color::White),
650 ),
651 ]),
652 Line::from(vec![
653 Span::styled(" Sent ", Style::default().fg(MUTED)),
654 Span::styled(
655 format_tokens(state.total_original.saturating_sub(state.total_saved)),
656 Style::default().fg(Color::White),
657 ),
658 ]),
659 Line::from(vec![
660 Span::styled(" Saved ", Style::default().fg(MUTED)),
661 Span::styled(format!("${cost:.3}"), Style::default().fg(GREEN)),
662 ]),
663 Line::from(""),
664 Line::from(Span::styled(
665 " q=quit Tab=focus 1-4=panel",
666 Style::default().fg(Color::Rgb(50, 50, 70)),
667 )),
668 ];
669
670 let paragraph = Paragraph::new(lines).block(block);
671 f.render_widget(paragraph, area);
672}
673
674fn format_tokens(n: u64) -> String {
675 if n >= 1_000_000 {
676 format!("{:.1}M", n as f64 / 1_000_000.0)
677 } else if n >= 1_000 {
678 format!("{:.1}K", n as f64 / 1_000.0)
679 } else {
680 format!("{n}")
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 fn mk_state() -> AppState {
689 AppState {
690 events: Vec::new(),
691 total_saved: 0,
692 total_original: 0,
693 cache_hits: 0,
694 total_calls: 0,
695 files: std::collections::HashMap::new(),
696 gain_score: None,
697 last_gain_refresh: Instant::now(),
698 quit: false,
699 focus: 0,
700 }
701 }
702
703 #[test]
704 fn ingest_toolcall_with_path_populates_heatmap() {
705 let mut s = mk_state();
706 s.ingest(vec![LeanCtxEvent {
707 id: 1,
708 timestamp: "t".to_string(),
709 kind: EventKind::ToolCall {
710 tool: "ctx_read".to_string(),
711 tokens_original: 100,
712 tokens_saved: 80,
713 mode: Some("full".to_string()),
714 duration_ms: 1,
715 path: Some("src/main.rs".to_string()),
716 },
717 }]);
718
719 let entry = s.files.get("src/main.rs").expect("file entry missing");
720 assert_eq!(entry.access_count, 1);
721 assert_eq!(entry.tokens_saved, 80);
722 }
723
724 #[test]
725 fn ingest_compression_counts_access_without_fake_tokens() {
726 let mut s = mk_state();
727 s.ingest(vec![LeanCtxEvent {
728 id: 1,
729 timestamp: "t".to_string(),
730 kind: EventKind::Compression {
731 path: "src/lib.rs".to_string(),
732 before_lines: 100,
733 after_lines: 10,
734 strategy: "entropy".to_string(),
735 kept_line_count: 10,
736 removed_line_count: 90,
737 },
738 }]);
739
740 let entry = s.files.get("src/lib.rs").expect("file entry missing");
741 assert_eq!(entry.access_count, 1);
742 assert_eq!(entry.tokens_saved, 0);
743 }
744}