diff --git a/crates/tui/src/commands/attachment.rs b/crates/tui/src/commands/attachment.rs index ad72ca93..82abe0d4 100644 --- a/crates/tui/src/commands/attachment.rs +++ b/crates/tui/src/commands/attachment.rs @@ -75,6 +75,7 @@ mod tests { allow_shell: false, use_alt_screen: false, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: tmpdir.path().join("skills"), memory_path: tmpdir.path().join("memory.md"), diff --git a/crates/tui/src/commands/config.rs b/crates/tui/src/commands/config.rs index c71d0c83..1f943f38 100644 --- a/crates/tui/src/commands/config.rs +++ b/crates/tui/src/commands/config.rs @@ -437,6 +437,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/commands/core.rs b/crates/tui/src/commands/core.rs index c84111ca..09f46344 100644 --- a/crates/tui/src/commands/core.rs +++ b/crates/tui/src/commands/core.rs @@ -215,6 +215,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("/tmp/test-skills"), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/commands/debug.rs b/crates/tui/src/commands/debug.rs index dd541ba7..7eb3fbbd 100644 --- a/crates/tui/src/commands/debug.rs +++ b/crates/tui/src/commands/debug.rs @@ -128,6 +128,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("/tmp/test-skills"), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/commands/init.rs b/crates/tui/src/commands/init.rs index f939f6e8..13985e20 100644 --- a/crates/tui/src/commands/init.rs +++ b/crates/tui/src/commands/init.rs @@ -166,6 +166,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: tmpdir.path().join("skills"), memory_path: tmpdir.path().join("memory.md"), diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index 0788aaee..ac8fccf2 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -526,6 +526,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/commands/note.rs b/crates/tui/src/commands/note.rs index 2d2a3e5c..85d8a089 100644 --- a/crates/tui/src/commands/note.rs +++ b/crates/tui/src/commands/note.rs @@ -63,6 +63,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: tmpdir.path().join("skills"), memory_path: tmpdir.path().join("memory.md"), diff --git a/crates/tui/src/commands/provider.rs b/crates/tui/src/commands/provider.rs index 7846d70e..f759b56e 100644 --- a/crates/tui/src/commands/provider.rs +++ b/crates/tui/src/commands/provider.rs @@ -82,6 +82,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/commands/queue.rs b/crates/tui/src/commands/queue.rs index 06207ab3..110a6170 100644 --- a/crates/tui/src/commands/queue.rs +++ b/crates/tui/src/commands/queue.rs @@ -142,6 +142,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: tmpdir.path().join("skills"), memory_path: tmpdir.path().join("memory.md"), diff --git a/crates/tui/src/commands/review.rs b/crates/tui/src/commands/review.rs index 6ba9aa39..8c0a07de 100644 --- a/crates/tui/src/commands/review.rs +++ b/crates/tui/src/commands/review.rs @@ -76,6 +76,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: tmpdir.path().join("skills"), memory_path: tmpdir.path().join("memory.md"), diff --git a/crates/tui/src/commands/session.rs b/crates/tui/src/commands/session.rs index 6b643e56..0fd105bf 100644 --- a/crates/tui/src/commands/session.rs +++ b/crates/tui/src/commands/session.rs @@ -200,6 +200,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: tmpdir.path().join("skills"), memory_path: tmpdir.path().join("memory.md"), diff --git a/crates/tui/src/commands/skills.rs b/crates/tui/src/commands/skills.rs index 4124e449..3b200a6a 100644 --- a/crates/tui/src/commands/skills.rs +++ b/crates/tui/src/commands/skills.rs @@ -122,6 +122,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: tmpdir.path().join("skills"), memory_path: tmpdir.path().join("memory.md"), diff --git a/crates/tui/src/commands/task.rs b/crates/tui/src/commands/task.rs index ecc2319c..dcfcbf91 100644 --- a/crates/tui/src/commands/task.rs +++ b/crates/tui/src/commands/task.rs @@ -55,6 +55,7 @@ mod tests { allow_shell: false, use_alt_screen: false, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 2, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/core/capacity.rs b/crates/tui/src/core/capacity.rs index e9aa3ff9..576cc84a 100644 --- a/crates/tui/src/core/capacity.rs +++ b/crates/tui/src/core/capacity.rs @@ -693,4 +693,40 @@ mod tests { assert_eq!(decision.action, GuardrailAction::NoIntervention); assert!(decision.cooldown_blocked); } + + /// Hot-path microbench for `compute_profile`. Run with: + /// + /// ```text + /// cargo test -p deepseek-tui --release capacity::tests::bench_compute_profile -- --ignored --nocapture + /// ``` + /// + /// Establishes a baseline cost so we can detect regressions when the + /// observation cadence is high (50+ message turns × per-step calls). Adds + /// no dev-deps; we measure with `Instant` and print rather than gating CI. + #[test] + #[ignore] + fn bench_compute_profile() { + use std::time::Instant; + + for &window_len in &[16usize, 64, 256, 1024] { + let mut window: VecDeque = VecDeque::with_capacity(window_len); + for i in 0..window_len { + #[allow(clippy::cast_precision_loss)] + window.push_back((i as f64).sin() * 0.5); + } + + let iters = 100_000usize; + let start = Instant::now(); + for _ in 0..iters { + let profile = compute_profile(&window); + std::hint::black_box(profile); + } + let elapsed = start.elapsed(); + let per_call_ns = elapsed.as_nanos() as f64 / iters as f64; + println!( + "compute_profile window={window_len:>4} total={:?} per-call={per_call_ns:>8.0}ns", + elapsed + ); + } + } } diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 50f2f8cb..6169d45e 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -2731,6 +2731,9 @@ async fn run_interactive( ); let use_alt_screen = should_use_alt_screen(cli, config); let use_mouse_capture = should_use_mouse_capture(cli, config, use_alt_screen); + let use_bracketed_paste = crate::settings::Settings::load() + .map(|s| s.bracketed_paste) + .unwrap_or(true); tui::run_tui( config, @@ -2740,6 +2743,7 @@ async fn run_interactive( allow_shell: cli.yolo || config.allow_shell(), use_alt_screen, use_mouse_capture, + use_bracketed_paste, skills_dir: config.skills_dir(), memory_path: config.memory_path(), notes_path: config.notes_path(), diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index ddc9f726..bb8d980b 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -21,6 +21,10 @@ pub struct Settings { pub low_motion: bool, /// Enable fancy footer animations (water-spout strip, pulsing text) pub fancy_animations: bool, + /// Enable terminal bracketed-paste mode. Default true. Disable if your + /// terminal mishandles the `\e[?2004h` escape (rare; some legacy + /// terminals over SSH+screen multiplex without the cap). + pub bracketed_paste: bool, /// Show thinking blocks from the model pub show_thinking: bool, /// Show detailed tool output @@ -50,6 +54,7 @@ impl Default for Settings { calm_mode: false, low_motion: false, fancy_animations: false, + bracketed_paste: true, show_thinking: true, show_tool_details: true, composer_density: "comfortable".to_string(), @@ -142,6 +147,9 @@ impl Settings { "fancy_animations" | "fancy" | "animations" => { self.fancy_animations = parse_bool(value)?; } + "bracketed_paste" | "paste" => { + self.bracketed_paste = parse_bool(value)?; + } "show_thinking" | "thinking" => { self.show_thinking = parse_bool(value)?; } @@ -251,6 +259,7 @@ impl Settings { lines.push(format!(" calm_mode: {}", self.calm_mode)); lines.push(format!(" low_motion: {}", self.low_motion)); lines.push(format!(" fancy_animations: {}", self.fancy_animations)); + lines.push(format!(" bracketed_paste: {}", self.bracketed_paste)); lines.push(format!(" show_thinking: {}", self.show_thinking)); lines.push(format!(" show_tool_details: {}", self.show_tool_details)); lines.push(format!(" composer_density: {}", self.composer_density)); @@ -286,6 +295,10 @@ impl Settings { "fancy_animations", "Fancy footer animations (water-spout strip): on/off", ), + ( + "bracketed_paste", + "Terminal bracketed-paste mode: on/off (rare to disable)", + ), ("show_thinking", "Show model thinking: on/off"), ("show_tool_details", "Show detailed tool output: on/off"), ( diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 5261f4b3..a1953439 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -321,6 +321,10 @@ pub struct TuiOptions { pub use_alt_screen: bool, /// Capture mouse input for internal scrolling/selection. pub use_mouse_capture: bool, + /// Enable terminal bracketed-paste mode (OSC `?2004h` / `?2004l`). Defaults + /// on; settable via `bracketed_paste = false` in `settings.toml` for the + /// rare terminal that mishandles it. + pub use_bracketed_paste: bool, /// Maximum number of concurrent sub-agents. pub max_subagents: usize, #[allow(dead_code)] @@ -397,6 +401,7 @@ pub struct App { pub skills_dir: PathBuf, pub use_alt_screen: bool, pub use_mouse_capture: bool, + pub use_bracketed_paste: bool, #[allow(dead_code)] pub system_prompt: Option, pub input_history: Vec, @@ -638,6 +643,7 @@ impl App { allow_shell, use_alt_screen, use_mouse_capture, + use_bracketed_paste, max_subagents, skills_dir: global_skills_dir, memory_path: _, @@ -745,6 +751,7 @@ impl App { skills_dir, use_alt_screen, use_mouse_capture, + use_bracketed_paste, system_prompt: None, input_history: Vec::new(), history_index: None, @@ -1773,6 +1780,7 @@ mod tests { allow_shell: yolo, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/tui/model_picker.rs b/crates/tui/src/tui/model_picker.rs index 7b4936b3..f52f120f 100644 --- a/crates/tui/src/tui/model_picker.rs +++ b/crates/tui/src/tui/model_picker.rs @@ -347,6 +347,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index a99bb445..2bddfc3b 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -124,6 +124,7 @@ const SIDEBAR_VISIBLE_MIN_WIDTH: u16 = 100; pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> { let use_alt_screen = options.use_alt_screen; let use_mouse_capture = options.use_mouse_capture; + let use_bracketed_paste = options.use_bracketed_paste; enable_raw_mode()?; let mut stdout = io::stdout(); if use_alt_screen { @@ -132,7 +133,9 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> { if use_mouse_capture { execute!(stdout, EnableMouseCapture)?; } - execute!(stdout, EnableBracketedPaste)?; + if use_bracketed_paste { + execute!(stdout, EnableBracketedPaste)?; + } let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let event_broker = EventBroker::new(); @@ -295,7 +298,9 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> { if use_mouse_capture { execute!(terminal.backend_mut(), DisableMouseCapture)?; } - execute!(terminal.backend_mut(), DisableBracketedPaste)?; + if use_bracketed_paste { + execute!(terminal.backend_mut(), DisableBracketedPaste)?; + } terminal.show_cursor()?; result @@ -688,13 +693,23 @@ async fn run_event_loop( } EngineEvent::PauseEvents => { if !event_broker.is_paused() { - pause_terminal(terminal, app.use_alt_screen, app.use_mouse_capture)?; + pause_terminal( + terminal, + app.use_alt_screen, + app.use_mouse_capture, + app.use_bracketed_paste, + )?; event_broker.pause_events(); } } EngineEvent::ResumeEvents => { if event_broker.is_paused() { - resume_terminal(terminal, app.use_alt_screen, app.use_mouse_capture)?; + resume_terminal( + terminal, + app.use_alt_screen, + app.use_mouse_capture, + app.use_bracketed_paste, + )?; event_broker.resume_events(); } } @@ -3034,6 +3049,7 @@ fn pause_terminal( terminal: &mut Terminal>, use_alt_screen: bool, use_mouse_capture: bool, + use_bracketed_paste: bool, ) -> Result<()> { disable_raw_mode()?; if use_alt_screen { @@ -3042,7 +3058,9 @@ fn pause_terminal( if use_mouse_capture { execute!(terminal.backend_mut(), DisableMouseCapture)?; } - execute!(terminal.backend_mut(), DisableBracketedPaste)?; + if use_bracketed_paste { + execute!(terminal.backend_mut(), DisableBracketedPaste)?; + } Ok(()) } @@ -3050,6 +3068,7 @@ fn resume_terminal( terminal: &mut Terminal>, use_alt_screen: bool, use_mouse_capture: bool, + use_bracketed_paste: bool, ) -> Result<()> { enable_raw_mode()?; if use_alt_screen { @@ -3058,7 +3077,9 @@ fn resume_terminal( if use_mouse_capture { execute!(terminal.backend_mut(), EnableMouseCapture)?; } - execute!(terminal.backend_mut(), EnableBracketedPaste)?; + if use_bracketed_paste { + execute!(terminal.backend_mut(), EnableBracketedPaste)?; + } terminal.clear()?; Ok(()) } diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 6b8175d1..1e62150c 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -117,6 +117,7 @@ fn create_test_app() -> App { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index f64ea7a7..5082e0bc 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -1521,6 +1521,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/tui/widgets/footer.rs b/crates/tui/src/tui/widgets/footer.rs index cb0a4fa9..c7c32f4e 100644 --- a/crates/tui/src/tui/widgets/footer.rs +++ b/crates/tui/src/tui/widgets/footer.rs @@ -371,6 +371,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"), diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 0afb9eb8..cca874ca 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -1189,6 +1189,7 @@ mod tests { allow_shell: false, use_alt_screen: true, use_mouse_capture: false, + use_bracketed_paste: true, max_subagents: 1, skills_dir: PathBuf::from("."), memory_path: PathBuf::from("memory.md"),