1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Owned, `Send`-safe snapshot of everything `draw()` reads.
//!
//! Built on the main task each frame in [`super::draw::build_render_model`].
//! Shipped over a latest-wins slot to a dedicated render `std::thread` which
//! calls [`super::draw::render_frame`] — the two-step split is fully in place.
//!
//! **Invariant**: zero borrows back into `App`. If `render_frame` compiles
//! without an `&App` parameter, the snapshot is proven complete.
use std::sync::Arc;
use super::models::ModelsModalState;
use super::plugins::PluginsModalState;
use super::settings::{RuntimeSnapshot, SettingsState};
use super::sidecar::SidecarUiStatus;
/// The per-frame snapshot shipped to the render side.
pub(crate) struct RenderModel {
// ── Header / status ──────────────────────────────────────────────────────
pub(crate) status_text: Option<String>,
pub(crate) streaming: bool,
pub(crate) spinner_frame: usize,
/// Sidecar pills projected out of the live `HashMap<String, SidecarUiState>`.
/// Ordered by importance desc then display_name asc, as computed on the
/// main side. Strips the `Child` process entirely.
pub(crate) sidecar_pills: Vec<SidecarPillSnap>,
/// `runtime.model()` string — cheap clone, avoids shipping the Runtime.
pub(crate) runtime_model: String,
/// `runtime.thinking_level()` string.
pub(crate) runtime_thinking: String,
// ── Messages pane ────────────────────────────────────────────────────────
/// Pre-rendered, width-keyed line cache. `Arc` so snapshot clone is a
/// refcount bump, not a deep copy.
pub(crate) lines: Arc<[ratatui::text::Line<'static>]>,
/// Content width the cache was built at (diagnostic / assert use by the render thread).
#[allow(dead_code)]
pub(crate) lines_width: usize,
/// Final scroll offset, already clamped on the main side.
pub(crate) scroll_back: u16,
/// Text-selection overlay coordinates (cell-level), pre-computed.
pub(crate) selection: Option<(u16, u16, u16, u16)>,
/// `true` when there are no messages — drives logo visibility.
pub(crate) messages_empty: bool,
// ── Logo / boot animation ─────────────────────────────────────────────────
pub(crate) logo_build_t: Option<f64>,
pub(crate) logo_dismiss_t: Option<f64>,
// ── Subagent panel ────────────────────────────────────────────────────────
pub(crate) subagents: Vec<SubagentSnap>,
// ── Active-task progress bar ──────────────────────────────────────────────
/// Arc snapshot of `app.active_tasks` — refcount bump, not a deep clone.
pub(crate) active_tasks: std::sync::Arc<synaps_cli::extensions::active_tasks::ActiveTasks>,
// ── Input box ────────────────────────────────────────────────────────────
pub(crate) input: String,
pub(crate) cursor_pos: usize,
/// Pre-computed ghost-hint for slash-command completion.
pub(crate) ghost_hint: Option<GhostHint>,
// ── Footer ───────────────────────────────────────────────────────────────
pub(crate) show_full_output: bool,
pub(crate) session_cost: f64,
pub(crate) total_input_tokens: u64,
pub(crate) total_output_tokens: u64,
pub(crate) total_cache_read_tokens: u64,
pub(crate) total_cache_creation_tokens: u64,
pub(crate) total_cache_write_1h: u64,
pub(crate) last_turn_context: u64,
pub(crate) last_turn_context_window: u64,
// ── Toasts ───────────────────────────────────────────────────────────────
pub(crate) toasts: Vec<super::toast::Toast>,
// ── Modals ───────────────────────────────────────────────────────────────
/// `Some((state, runtime_snap))` when the settings modal is open.
pub(crate) settings: Option<(SettingsState, RuntimeSnapshot)>,
pub(crate) plugins: Option<PluginsModalState>,
pub(crate) models: Option<ModelsModalState>,
/// Snapshot of `HelpFindState`. `help_find::render` takes
/// `&mut HelpFindState`; `visible_height` is pre-computed on the main side
/// before snapshotting so the modal's scroll window is authoritative.
pub(crate) help_find: Option<synaps_cli::help::HelpFindState>,
// ── Secret prompt modal ───────────────────────────────────────────────────
pub(crate) secret_prompt: Option<SecretPromptSnap>,
// ── Stack-order modal draw list (P7.8) ────────────────────────────────────
/// Snapshot of `app.modal_stack` (bottom → top). `render_frame_into`
/// iterates this to paint modals in stack order, replacing the former
/// hardcoded paint order. SecretPrompt therefore paints LAST (topmost)
/// when it coexists with a modal — the one deliberate z-order fix (§5.5).
pub(crate) modal_order: Vec<super::focus::PaneId>,
// ── Edge-scrub geometry ───────────────────────────────────────────────────
/// Pre-computed `protected_bottom_rows` for `scrub_crossterm_terminal_edges`.
pub(crate) protected_bottom_rows: u16,
}
// ── Projection types ─────────────────────────────────────────────────────────
/// Render-safe projection of `SidecarUiState` — strips the `Child` process.
#[derive(Clone)]
pub(crate) struct SidecarPillSnap {
/// Plugin id — retained for ordering / debug; not read by renderer.
#[allow(dead_code)]
pub(crate) plugin_id: String,
pub(crate) display_name: Option<String>,
pub(crate) status: SidecarUiStatus,
pub(crate) armed: bool,
}
/// Render-safe projection of `SubagentState`.
#[derive(Clone)]
pub(crate) struct SubagentSnap {
pub(crate) name: String,
pub(crate) status: String,
/// `duration_secs.unwrap_or_else(|| start_time.elapsed().as_secs_f64())`
/// pre-computed so the render thread doesn't need `Instant`.
pub(crate) elapsed_secs: f64,
pub(crate) done: bool,
}
/// Pre-computed ghost-hint for slash-command completion in the input box.
#[derive(Clone)]
pub(crate) struct GhostHint {
/// Either the suffix after the user's partial (prefix match) or
/// `" → /command"` form.
pub(crate) ghost_text: String,
/// Multi-match badge: `"N matches · Tab search"`.
pub(crate) match_badge: Option<String>,
}
/// Render-safe projection of `PendingSecretPrompt`.
#[derive(Clone)]
pub(crate) struct SecretPromptSnap {
pub(crate) title: String,
pub(crate) prompt: String,
pub(crate) masked_buffer_chars: usize,
}