Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/noa-app/src/app/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ pub struct AppConfig {
/// `auto-approve`: seed new tabs with agent-CLI auto approval enabled.
/// Runtime toggles are still per-tab.
pub auto_approve: bool,
/// `send-selection-send-enter`: follow the send-selection picker's paste
/// with an Enter write so the pasted text is submitted immediately.
pub send_selection_send_enter: bool,
/// Raw `keybind = ...` entries from config. Parsed into the runtime
/// [`crate::commands::KeybindEngine`] by `App::new` and live reload.
pub keybinds: Vec<noa_config::KeybindConfig>,
Expand Down Expand Up @@ -247,6 +250,7 @@ impl AppConfig {
audible_bell_when_unfocused: config.audible_bell_when_unfocused,
audible_bell_dock_bounce: config.audible_bell_dock_bounce,
auto_approve: config.auto_approve,
send_selection_send_enter: config.send_selection_send_enter,
keybinds: config.keybinds,
server_enable: config.server_enable,
server_port: config.server_port,
Expand Down
38 changes: 34 additions & 4 deletions crates/noa-app/src/app/input_ops/clipboard_confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl App {
pane_id: PaneId,
text: String,
) {
self.paste_text_to_pane_with_confirm_window(window_id, window_id, pane_id, text);
self.paste_text_to_pane_with_confirm_window(window_id, window_id, pane_id, text, false);
}

pub(in crate::app) fn paste_text_to_pane_with_confirm_window(
Expand All @@ -70,6 +70,7 @@ impl App {
window_id: WindowId,
pane_id: PaneId,
text: String,
then_enter: bool,
) {
let bracketed_paste = self.bracketed_paste(window_id, pane_id);
// Paste protection: confirm before sending content that could run a
Expand All @@ -87,27 +88,55 @@ impl App {
window_id,
pane_id,
text,
then_enter,
},
);
return;
}
self.write_paste_text_to_pane(window_id, pane_id, &text);
self.write_paste_text_to_pane(window_id, pane_id, &text, then_enter);
}

pub(in crate::app) fn write_paste_text_to_pane(
&mut self,
window_id: WindowId,
pane_id: PaneId,
text: &str,
then_enter: bool,
) {
let bracketed_paste = self.bracketed_paste(window_id, pane_id);
if let Some(bytes) = input::encode_paste(text, bracketed_paste) {
if let Some(mut bytes) = input::encode_paste(text, bracketed_paste) {
// The trailing Enter is appended after the paste encoding — outside
// the bracketed-paste wrapper, where a newline is inert data — so
// the pair travels in one queue element and is admitted or dropped
// atomically. Written separately, a backpressured input queue could
// drop the large paste yet admit the tiny Enter, executing whatever
// already sits on the prompt line. It is encoded under the target
// pane's Kitty flags: a report-all-keys client reads Enter as
// `CSI 13 u`, not a legacy CR. Skipped entirely when nothing was
// pasted, for the same run-the-stale-prompt-line reason.
if then_enter {
bytes.extend_from_slice(&input::encode_enter_key(
self.pane_kitty_keyboard_flags(window_id, pane_id),
));
}
self.mark_pane_paste_input(window_id, pane_id);
self.snap_pane_viewport_to_bottom(window_id, pane_id);
self.write_pane_pty_bytes(window_id, pane_id, bytes);
}
}

/// The target pane's active Kitty keyboard progressive-enhancement flags
/// (`0` when the pane is gone). Per-pane counterpart of the focused-surface
/// `App::kitty_keyboard_flags` — the send-selection target need not be the
/// focused pane.
fn pane_kitty_keyboard_flags(&self, window_id: WindowId, pane_id: PaneId) -> u8 {
self.windows
.get(&window_id)
.and_then(|state| state.surfaces.get(&pane_id))
.map(|surface| surface.terminal.lock().kitty_keyboard_flags())
.unwrap_or(0)
}

pub(in crate::app) fn bracketed_paste(&self, window_id: WindowId, pane_id: PaneId) -> bool {
self.windows
.get(&window_id)
Expand Down Expand Up @@ -207,7 +236,8 @@ impl App {
window_id,
pane_id,
text,
} => self.write_paste_text_to_pane(window_id, pane_id, &text),
then_enter,
} => self.write_paste_text_to_pane(window_id, pane_id, &text, then_enter),
ConfirmAction::ClipboardRead {
window_id,
pane_id,
Expand Down
1 change: 1 addition & 0 deletions crates/noa-app/src/app/input_ops/overlays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ impl App {
target.window_id,
target.pane_id,
session.selected_text,
self.config.send_selection_send_enter,
);
}

Expand Down
27 changes: 21 additions & 6 deletions crates/noa-app/src/app/input_ops/theme_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ impl App {
sidebar_font_size: self.config.sidebar_font_size,
quick_terminal_size: quick_terminal_height_fraction(self.config.quick_terminal_size),
confirm_quit: self.config.confirm_quit,
send_selection_send_enter: self.config.send_selection_send_enter,
font_family,
available_font_families,
scrollback_limit: self.config.scrollback_limit,
Expand Down Expand Up @@ -966,9 +967,9 @@ impl App {

/// Mirror the just-committed runtime rows (font-size, background-opacity,
/// background-blur-radius, background-image settings, cursor-style,
/// sidebar-preview-lines, quick-terminal-size, confirm-quit) into
/// `self.config` so a future reopen of the overlay, or the next quick-
/// terminal toggle, shows the new value.
/// sidebar-preview-lines, quick-terminal-size, confirm-quit,
/// send-selection-send-enter) into `self.config` so a future reopen of
/// the overlay, or the next quick-terminal toggle, shows the new value.
/// The restart-only rows are deliberately excluded: nothing on screen
/// actually changes for them until a restart, so leaving `self.config` at
/// its pre-commit value keeps it truthful to what the user still sees, even
Expand Down Expand Up @@ -1040,6 +1041,9 @@ impl App {
RowDraft::ConfirmQuit(v) => {
self.config.confirm_quit = *v;
}
RowDraft::SendSelectionSendEnter(v) => {
self.config.send_selection_send_enter = *v;
}
// Commit-only rows: intentionally not mirrored (see the doc
// comment above).
RowDraft::FontFamily(_)
Expand Down Expand Up @@ -1224,9 +1228,10 @@ fn resolve_current_theme(config: &AppConfig, appearance: winit::window::Theme) -
}

/// TSV2-1 (judge, CONFIRMED): [`sync_config_from_committed_live_rows`]
/// mirrors `confirm-quit` and (via [`sync_quick_terminal_size_from_committed_rows`])
/// mirrors `confirm-quit`, `send-selection-send-enter`, and (via
/// [`sync_quick_terminal_size_from_committed_rows`])
/// `quick-terminal-size` into `self.config` on a successful commit, because
/// both are read back out of `self.config` at runtime rather than applied
/// all are read back out of `self.config` at runtime rather than applied
/// live like the R-8 `is_live` rows — an Undo that only rewrote the config
/// *file* left the running session's `self.config` still holding the
/// committed (unwanted) value. Standalone (rather than inlined into
Expand All @@ -1244,6 +1249,7 @@ fn sync_reverted_confirm_quit_and_quick_terminal_size(
revert: &crate::theme_settings::RevertValues,
) {
config.confirm_quit = revert.confirm_quit;
config.send_selection_send_enter = revert.send_selection_send_enter;
config.quick_terminal_size =
quick_terminal_size_from_height_fraction(revert.quick_terminal_size);
}
Expand Down Expand Up @@ -1470,6 +1476,7 @@ mod commit_theme_settings_tests {
sidebar_font_size: noa_config::DEFAULT_SIDEBAR_FONT_SIZE,
quick_terminal_size: 0.4,
confirm_quit: true,
send_selection_send_enter: false,
font_family: "Menlo".to_string(),
available_font_families: Vec::new(),
scrollback_limit: noa_config::DEFAULT_SCROLLBACK_LIMIT,
Expand Down Expand Up @@ -1523,8 +1530,10 @@ mod commit_theme_settings_tests {
noa_config::ConfigOverrides::default(),
);
// Simulate the post-commit state a real session would have left
// `self.config` in: confirm-quit flipped off, quick-terminal grown.
// `self.config` in: confirm-quit flipped off, send-selection Enter
// flipped on, quick-terminal grown.
config.confirm_quit = false;
config.send_selection_send_enter = true;
config.quick_terminal_size = quick_terminal_size_from_height_fraction(0.9);

let revert = crate::theme_settings::RevertValues {
Expand All @@ -1547,6 +1556,7 @@ mod commit_theme_settings_tests {
window_padding_y: 2.0,
macos_titlebar_style: noa_config::MacosTitlebarStyle::Native,
confirm_quit: true,
send_selection_send_enter: false,
font_family: "Menlo".to_string(),
};
sync_reverted_confirm_quit_and_quick_terminal_size(&mut config, &revert);
Expand All @@ -1555,6 +1565,10 @@ mod commit_theme_settings_tests {
config.confirm_quit,
"confirm-quit must revert to the pre-open value"
);
assert!(
!config.send_selection_send_enter,
"send-selection-send-enter must revert to the pre-open value"
);
assert!(
(quick_terminal_height_fraction(config.quick_terminal_size) - 0.4).abs() < 0.001,
"quick-terminal-size must revert to the pre-open value"
Expand Down Expand Up @@ -1605,6 +1619,7 @@ mod commit_theme_settings_tests {
sidebar_font_size: noa_config::DEFAULT_SIDEBAR_FONT_SIZE,
quick_terminal_size: 0.4,
confirm_quit: true,
send_selection_send_enter: false,
font_family: "Menlo".to_string(),
available_font_families: Vec::new(),
scrollback_limit: noa_config::DEFAULT_SCROLLBACK_LIMIT,
Expand Down
5 changes: 5 additions & 0 deletions crates/noa-app/src/app/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,7 @@ mod theme_settings_session_tests {
sidebar_font_size: noa_config::DEFAULT_SIDEBAR_FONT_SIZE,
quick_terminal_size: 0.4,
confirm_quit: true,
send_selection_send_enter: false,
font_family: "Menlo".to_string(),
available_font_families: Vec::new(),
scrollback_limit: noa_config::DEFAULT_SCROLLBACK_LIMIT,
Expand Down Expand Up @@ -857,6 +858,10 @@ pub(super) enum ConfirmAction {
window_id: WindowId,
pane_id: PaneId,
text: String,
/// Follow the paste with an Enter (`\r`) write — the send-selection
/// picker's `send-selection-send-enter` behavior, deferred with the
/// paste itself.
then_enter: bool,
},
/// Fulfill an OSC 52 clipboard read: read the clipboard now and write the
/// base64 reply to the pane's pty.
Expand Down
8 changes: 8 additions & 0 deletions crates/noa-app/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,11 @@ fn show_config_output(config: &StartupConfig) -> String {
&config.audible_bell_dock_bounce.to_string(),
);
push_line(&mut out, "auto-approve", &config.auto_approve.to_string());
push_line(
&mut out,
"send-selection-send-enter",
&config.send_selection_send_enter.to_string(),
);
push_repeatable_lines(
&mut out,
"keybind",
Expand Down Expand Up @@ -716,6 +721,7 @@ mod tests {
assert!(output.contains("audible-bell-when-unfocused = false\n"));
assert!(output.contains("audible-bell-dock-bounce = false\n"));
assert!(output.contains("auto-approve = false\n"));
assert!(output.contains("send-selection-send-enter = false\n"));
assert!(
output.lines().all(|line| line.contains(" = ")),
"every line must be `key = value`"
Expand Down Expand Up @@ -751,6 +757,7 @@ mod tests {
audible_bell_when_unfocused: true,
audible_bell_dock_bounce: true,
auto_approve: true,
send_selection_send_enter: true,
font: noa_config::FontConfig {
families: vec!["JetBrains Mono".to_string(), "Menlo".to_string()],
features: vec![noa_config::FontFeature {
Expand Down Expand Up @@ -791,6 +798,7 @@ mod tests {
assert!(output.contains("audible-bell-when-unfocused = true\n"));
assert!(output.contains("audible-bell-dock-bounce = true\n"));
assert!(output.contains("auto-approve = true\n"));
assert!(output.contains("send-selection-send-enter = true\n"));
assert!(output.contains("font-family = JetBrains Mono\n"));
assert!(output.contains("font-family = Menlo\n"));
assert!(output.contains("font-feature = -liga\n"));
Expand Down
2 changes: 1 addition & 1 deletion crates/noa-app/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod text;
pub(crate) use ime::ImeState;
#[cfg(test)]
use key::encode_key;
pub(crate) use key::encode_key_with_modes;
pub(crate) use key::{encode_enter_key, encode_key_with_modes};
pub use paste::encode_paste;
pub(crate) use paste::{applescript_input_bytes, paste_is_unsafe, raw_input_bytes};
#[cfg(test)]
Expand Down
21 changes: 21 additions & 0 deletions crates/noa-app/src/input/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,27 @@ pub fn encode_key_with_modes(
}
}

/// Bytes an unmodified Enter press writes to the pty under the given Kitty
/// keyboard flags: `CSI 13 u` once report-all-keys is in effect, legacy CR
/// otherwise. Delegates to [`encode_key_with_modes`] so a synthesized Enter
/// (the send-selection trailing Enter) can never diverge from a typed one.
pub(crate) fn encode_enter_key(kitty_flags: u8) -> Vec<u8> {
encode_key_with_modes(
&Key::Named(NamedKey::Enter),
None,
None,
None,
ModifiersState::empty(),
true,
false,
false,
kitty_flags,
true,
false,
)
.expect("an unmodified Enter press always encodes")
}

/// The C0 byte for Ctrl+`c` under the legacy encoding: letters map to
/// 0x01..0x1a, plus the classic xterm symbol and digit mappings.
fn ctrl_c0_byte(c: char) -> Option<u8> {
Expand Down
16 changes: 16 additions & 0 deletions crates/noa-app/src/input/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1309,3 +1309,19 @@ fn kitty_keypad_uses_dedicated_codes_under_report_all() {
Some(b"\x1b[57400u".to_vec())
);
}

// The send-selection trailing Enter must match what a typed Enter sends: a
// report-all-keys client reads `CSI 13 u`, everything else the legacy CR.
#[test]
fn encode_enter_key_follows_kitty_flags() {
assert_eq!(encode_enter_key(0), b"\r".to_vec());
assert_eq!(encode_enter_key(KITTY_DISAMBIGUATE), b"\r".to_vec());
assert_eq!(
encode_enter_key(KITTY_REPORT_ALL_KEYS),
b"\x1b[13u".to_vec()
);
assert_eq!(
encode_enter_key(KITTY_REPORT_ALL_KEYS | KITTY_REPORT_EVENT_TYPES),
b"\x1b[13u".to_vec()
);
}
2 changes: 2 additions & 0 deletions crates/noa-app/src/macos_overlay/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn settings_init() -> ThemeSettingsInit {
sidebar_font_size: noa_config::DEFAULT_SIDEBAR_FONT_SIZE,
quick_terminal_size: 0.4,
confirm_quit: true,
send_selection_send_enter: false,
font_family: "Menlo".to_string(),
available_font_families: Vec::new(),
scrollback_limit: noa_config::DEFAULT_SCROLLBACK_LIMIT,
Expand Down Expand Up @@ -185,6 +186,7 @@ fn test_theme_settings_init() -> ThemeSettingsInit {
sidebar_font_size: noa_config::DEFAULT_SIDEBAR_FONT_SIZE,
quick_terminal_size: 0.4,
confirm_quit: true,
send_selection_send_enter: false,
font_family: "Menlo".to_string(),
available_font_families: Vec::new(),
scrollback_limit: noa_config::DEFAULT_SCROLLBACK_LIMIT,
Expand Down
Loading