use super::app::{App, SubagentState};
pub(crate) struct ViewInputs<'a> {
pub(crate) gamba_active: bool,
pub(crate) transcript: &'a mut super::transcript::TranscriptStore,
pub(crate) input: String,
pub(crate) cursor_pos: usize,
pub(crate) prompt_fx: super::neon_prompt::PromptFx,
pub(crate) streaming: bool,
pub(crate) spinner_frame: usize,
pub(crate) agent_name: &'a str,
pub(crate) status_text: &'a Option<String>,
pub(crate) logo_build_t: Option<f64>,
pub(crate) logo_dismiss_t: Option<f64>,
pub(crate) subagents: &'a [SubagentState],
pub(crate) sidecars: &'a std::collections::HashMap<String, super::sidecar::SidecarUiState>,
pub(crate) active_tasks: &'a std::sync::Arc<synaps_cli::extensions::active_tasks::ActiveTasks>,
pub(crate) toasts: &'a super::toast::ToastProvider,
pub(crate) session_cost: f64,
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,
pub(crate) settings: &'a Option<super::settings::SettingsState>,
pub(crate) plugins: &'a Option<super::plugins::PluginsModalState>,
pub(crate) models: &'a Option<super::models::ModelsModalState>,
pub(crate) help_find: &'a Option<synaps_cli::help::HelpFindState>,
pub(crate) effort: &'a Option<super::effort::EffortModalState>,
pub(crate) model_health:
&'a std::collections::HashMap<String, (synaps_cli::runtime::openai::ping::PingStatus, u64)>,
pub(crate) secret_prompts: &'a synaps_cli::tools::SecretPromptQueue,
pub(crate) modal_stack: &'a super::focus::ModalStack,
}
impl<'a> ViewInputs<'a> {
pub(crate) fn from_app(app: &'a mut App) -> Self {
let input = app.input_text();
let cursor_pos = app.cursor_char_pos();
let prompt_fx = app.prompt_clock.frame(app.clock.now(), app.streaming);
Self {
gamba_active: app.gamba_child.is_some(),
transcript: &mut app.transcript,
input,
cursor_pos,
prompt_fx,
streaming: app.streaming,
spinner_frame: app.spinner_frame,
agent_name: &app.agent_name,
status_text: &app.status_text,
logo_build_t: app.logo_build_t,
logo_dismiss_t: app.logo_dismiss_t,
subagents: &app.subagents,
sidecars: &app.sidecars,
active_tasks: &app.active_tasks,
toasts: &app.toasts,
session_cost: app.session_cost,
keepwarm_cost: app.keepwarm_cost,
total_input_tokens: app.total_input_tokens,
total_output_tokens: app.total_output_tokens,
total_cache_read_tokens: app.total_cache_read_tokens,
total_cache_creation_tokens: app.total_cache_creation_tokens,
total_cache_write_1h: app.total_cache_write_1h,
last_turn_context: app.last_turn_context,
last_turn_context_window: app.last_turn_context_window,
settings: &app.settings,
plugins: &app.plugins,
models: &app.models,
help_find: &app.help_find,
effort: &app.effort,
model_health: &app.model_health,
secret_prompts: &app.secret_prompts,
modal_stack: &app.modal_stack,
}
}
}
#[must_use = "the patch must be applied to App or the authoritative modal state desyncs"]
#[derive(Debug, Default)]
pub(crate) struct RenderPatch {
pub(crate) help_find_visible_height: Option<usize>,
}
impl RenderPatch {
pub(crate) fn apply(self, app: &mut App) {
if let Some(h) = self.help_find_visible_height {
if let Some(ref mut hf) = app.help_find {
hf.set_visible_height(h);
}
}
}
}
pub(crate) const INPUT_PREFIX_WIDTH: usize = 2;
pub(crate) fn input_wrap_info(input: &str, cursor_pos: usize, inner_width: u16) -> (u16, u16, u16) {
use super::text_metrics::char_width;
let w = (inner_width as usize).max(INPUT_PREFIX_WIDTH + 1);
let prefix_width = INPUT_PREFIX_WIDTH;
let mut row: u16 = 0;
let mut col: usize = prefix_width;
let mut cursor_row: u16 = 0;
let mut cursor_col: u16 = prefix_width as u16;
for (i, ch) in input.chars().enumerate() {
if i == cursor_pos {
cursor_row = row;
cursor_col = col as u16;
}
if ch == '\n' {
row += 1;
col = prefix_width;
continue;
}
let cw = char_width(ch);
if col + cw > w && col > prefix_width {
row += 1;
col = prefix_width;
}
col += cw;
}
if cursor_pos >= input.chars().count() {
cursor_row = row;
cursor_col = col as u16;
if col >= w {
cursor_row += 1;
cursor_col = prefix_width as u16;
}
}
let rows = (row + 1).max(cursor_row + 1);
(rows, cursor_row, cursor_col)
}
#[cfg(test)]
mod input_wrap_tests {
use super::input_wrap_info;
#[test]
fn soft_wrapped_rows_keep_the_hanging_indent() {
let (rows, cr, cc) = input_wrap_info("abcdefghij", 10, 10);
assert_eq!(
(rows, cr, cc),
(2, 1, 4),
"2 chars carried to row 2, after the indent"
);
}
#[test]
fn newline_rows_keep_the_hanging_indent() {
assert_eq!(input_wrap_info("ab\ncd", 5, 10), (2, 1, 4));
}
#[test]
fn cursor_on_the_wrap_boundary_gets_a_row() {
assert_eq!(input_wrap_info("abcdefgh", 8, 10), (2, 1, 2));
}
#[test]
fn empty_input() {
assert_eq!(input_wrap_info("", 0, 10), (1, 0, 2));
}
}