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
45 changes: 45 additions & 0 deletions app/src/terminal/model/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ use super::image_map::StoredImageMetadata;
use super::kitty::{KittyAction, KittyResponse};
use super::rich_content::RichContentType;
use super::secrets::RespectObfuscatedSecrets;
use super::session::command_executor;
use super::{ansi::InputBufferValue, block::SerializedAIMetadata};

use super::selection::ScrollDelta;
Expand Down Expand Up @@ -1561,6 +1562,43 @@ impl BlockList {
}
}

/// Removes the shell's echo of an in-band command if it was classified as
/// background output.
///
/// Live in-band commands are written only after
/// [`Self::start_active_block_for_in_band_command`] has started the active
/// block, so the shell's echo lands in that hidden block. Replayed PTY output
/// (e.g. a daemon session re-attach) carries no such client-side mark: the
/// echo arrives while the active block is not started, becomes background
/// output, and would otherwise stay visible in the block list.
///
/// Only a live, unfinished background block whose entire trimmed content is
/// exactly the echoed command line is removed. Any other background output is
/// left untouched.
fn remove_echoed_in_band_command(&mut self, command: &str) {
let command = command.trim();
if command.is_empty() {
return;
}
let Some(background_block) = self.background_block_mut() else {
return;
};
let contents = background_block
.output_grid()
.contents_to_string_with_secrets_unobfuscated(
false, /* include_escape_sequences */
None, /* max_rows */
);
let is_only_echoed_command = contents
.trim_end()
.strip_suffix(command)
.is_some_and(|preceding| preceding.trim().is_empty());
if is_only_echoed_command {
log::debug!("Removing echoed in-band command from background output");
self.remove_background_block();
}
}

/// The setter for Block::block_banner needs to update the block_heights SumTree in order to
/// keep that data structure in sync.
pub(in crate::terminal) fn set_active_block_banner(
Expand Down Expand Up @@ -3857,6 +3895,13 @@ impl ansi::Handler for BlockList {
EarlyOutput::preexec(self);
}

// In-band commands must never be visible. If their echo ended up as
// background output (replayed PTY output without the client-side
// in-band mark), drop it; the active block is hidden by its own preexec.
if command_executor::is_in_band_command(&data.command) {
self.remove_echoed_in_band_command(&data.command);
}

delegate_to_block!(self.preexec(data));
}

Expand Down
74 changes: 74 additions & 0 deletions app/src/terminal/model/blocks_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,80 @@ fn test_background_blocks_finished() {
}
}

/// A bootstrapped block list wide enough that the test commands fit on one row.
fn new_wide_bootstrapped_block_list() -> BlockList {
let block_sizes = BlockSize {
size: SizeInfo::new_without_font_metrics(10, 120),
..test_utils::block_size()
};
new_bootstrapped_block_list(
Some(block_sizes),
None,
ChannelEventListener::new_for_test(),
)
}

#[test]
fn test_replayed_in_band_command_echo_is_not_visible() {
let mut block_list = new_wide_bootstrapped_block_list();
let command = "warp_run_generator_command 1791414087539893 'compgen -c'";

// Replayed PTY output: the shell echoes the in-band command, but the client
// never started the active block for it (no
// `start_active_block_for_in_band_command`), so the echo is classified as
// background output.
input_string(&mut block_list, command);
block_list.carriage_return();
block_list.linefeed();
block_list.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
assert!(block_list.background_block_mut().is_some());

block_list.preexec(PreexecValue {
command: command.to_owned(),
});
assert!(block_list.background_block_mut().is_none());
assert!(block_list.active_block().is_in_band_command_block());

command_finished_and_precmd(&mut block_list);

let agent_view_state = block_list.agent_view_state();
for block in block_list.blocks() {
let contains_generator = block.command_to_string().contains(command)
|| block
.output_grid()
.contents_to_string(false, None)
.contains(command);
assert!(
!contains_generator || block.should_hide_block(agent_view_state),
"in-band command must not be visible in block {:?}",
block.index()
);
}
}

#[test]
fn test_in_band_preexec_keeps_unrelated_background_output() {
let mut block_list = new_wide_bootstrapped_block_list();
let command = "warp_run_generator_command 1791414087539893 'compgen -c'";

// Output of a background job, not the echo of the in-band command.
input_string(&mut block_list, "[1]+ Done sleep 1");
block_list.carriage_return();
block_list.linefeed();
block_list.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));

block_list.preexec(PreexecValue {
command: command.to_owned(),
});

let background_output = block_list
.background_block_mut()
.expect("unrelated background output must be kept")
.output_grid()
.contents_to_string(false, None);
assert!(background_output.contains("[1]+ Done sleep 1"));
}

#[test]
fn test_interleaves_background_with_gaps() {
let mut block_list =
Expand Down
Loading