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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! 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>,
/// This frame's neon-prompt animation snapshot.
pub(crate) prompt_fx: super::neon_prompt::PromptFx,
// ── Footer ───────────────────────────────────────────────────────────────
pub(crate) show_full_output: bool,
pub(crate) session_cost: f64,
/// Keep-warm share of `session_cost` (footer label).
pub(crate) keepwarm_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>,
/// Snapshot of the /effort lightbox state.
pub(crate) effort: Option<super::effort::EffortModalState>,
// ── 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,
/// Tool calls so far.
pub(crate) tools: u32,
/// First line of the result once done.
pub(crate) result: Option<String>,
}
/// 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`.
///
/// `kind` selects the modal: `Secret` → masked `password:` field;
/// `Confirm` → y/n dialog with the full (word-wrapped) body visible and no
/// input field at all (fail-closed: only `y` allows).
#[derive(Clone)]
pub(crate) struct SecretPromptSnap {
pub(crate) kind: synaps_cli::tools::PromptKind,
pub(crate) title: String,
pub(crate) prompt: String,
pub(crate) masked_buffer_chars: usize,
}