feat: unified exec footer (#8117)
# With `unified_exec` Known tools are correctly casted <img width="1150" height="312" alt="Screenshot 2025-12-16 at 19 27 28" src="https://github.com/user-attachments/assets/24150ee5-e88d-461b-a459-483c24784196" /> If a session exit the turn, we render it with the "Ran ..." <img width="1168" height="355" alt="Screenshot 2025-12-16 at 19 27 58" src="https://github.com/user-attachments/assets/3f00b60c-2d57-4f9d-a201-9cc8388957cb" /> If a session does not exit during the turn, it is closed at the end of the turn but this is not rendered <img width="642" height="342" alt="Screenshot 2025-12-16 at 19 34 37" src="https://github.com/user-attachments/assets/c2bd9283-7017-4915-ba73-c52199b0b28e" /> # Without `unified_exec` No changes <img width="740" height="603" alt="Screenshot 2025-12-16 at 19 31 21" src="https://github.com/user-attachments/assets/ca5d90fe-a9b2-42ba-bcd7-3e98c4ed22e8" />
This commit is contained in:
parent
ac6ba286aa
commit
f74e0cda92
7 changed files with 405 additions and 3 deletions
|
|
@ -3,6 +3,7 @@ use std::path::PathBuf;
|
|||
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::queued_user_messages::QueuedUserMessages;
|
||||
use crate::bottom_pane::unified_exec_footer::UnifiedExecFooter;
|
||||
use crate::render::renderable::FlexRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableItem;
|
||||
|
|
@ -41,6 +42,7 @@ mod queued_user_messages;
|
|||
mod scroll_state;
|
||||
mod selection_popup_common;
|
||||
mod textarea;
|
||||
mod unified_exec_footer;
|
||||
pub(crate) use feedback_view::FeedbackNoteView;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -79,6 +81,8 @@ pub(crate) struct BottomPane {
|
|||
|
||||
/// Inline status indicator shown above the composer while a task is running.
|
||||
status: Option<StatusIndicatorWidget>,
|
||||
/// Unified exec session summary shown above the composer.
|
||||
unified_exec_footer: UnifiedExecFooter,
|
||||
/// Queued user messages to show above the composer while a turn is running.
|
||||
queued_user_messages: QueuedUserMessages,
|
||||
context_window_percent: Option<i64>,
|
||||
|
|
@ -126,6 +130,7 @@ impl BottomPane {
|
|||
is_task_running: false,
|
||||
ctrl_c_quit_hint: false,
|
||||
status: None,
|
||||
unified_exec_footer: UnifiedExecFooter::new(),
|
||||
queued_user_messages: QueuedUserMessages::new(),
|
||||
esc_backtrack_hint: false,
|
||||
animations_enabled,
|
||||
|
|
@ -396,6 +401,12 @@ impl BottomPane {
|
|||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn set_unified_exec_sessions(&mut self, sessions: Vec<String>) {
|
||||
if self.unified_exec_footer.set_sessions(sessions) {
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update custom prompts available for the slash popup.
|
||||
pub(crate) fn set_custom_prompts(&mut self, prompts: Vec<CustomPrompt>) {
|
||||
self.composer.set_custom_prompts(prompts);
|
||||
|
|
@ -526,8 +537,14 @@ impl BottomPane {
|
|||
if let Some(status) = &self.status {
|
||||
flex.push(0, RenderableItem::Borrowed(status));
|
||||
}
|
||||
if !self.unified_exec_footer.is_empty() {
|
||||
flex.push(0, RenderableItem::Borrowed(&self.unified_exec_footer));
|
||||
}
|
||||
flex.push(1, RenderableItem::Borrowed(&self.queued_user_messages));
|
||||
if self.status.is_some() || !self.queued_user_messages.messages.is_empty() {
|
||||
if self.status.is_some()
|
||||
|| !self.unified_exec_footer.is_empty()
|
||||
|| !self.queued_user_messages.messages.is_empty()
|
||||
{
|
||||
flex.push(0, RenderableItem::Owned("".into()));
|
||||
}
|
||||
let mut flex2 = FlexRenderable::new();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
source: tui/src/bottom_pane/unified_exec_footer.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 50, height: 2 },
|
||||
content: [
|
||||
"Background terminal running: echo hello · rg "foo"",
|
||||
" src · 1 more running ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 28, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 29, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 39, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 42, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 29, y: 1, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 32, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 49, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
source: tui/src/bottom_pane/unified_exec_footer.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 50, height: 2 },
|
||||
content: [
|
||||
"Background terminal running: echo hello · rg "foo"",
|
||||
" src ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 28, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 29, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 39, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 42, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 29, y: 1, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 32, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
125
codex-rs/tui/src/bottom_pane/unified_exec_footer.rs
Normal file
125
codex-rs/tui/src/bottom_pane/unified_exec_footer.rs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::text_formatting::truncate_text;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
|
||||
const MAX_SESSION_LABEL_GRAPHEMES: usize = 48;
|
||||
const MAX_VISIBLE_SESSIONS: usize = 2;
|
||||
|
||||
pub(crate) struct UnifiedExecFooter {
|
||||
sessions: Vec<String>,
|
||||
}
|
||||
|
||||
impl UnifiedExecFooter {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
sessions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_sessions(&mut self, sessions: Vec<String>) -> bool {
|
||||
if self.sessions == sessions {
|
||||
return false;
|
||||
}
|
||||
self.sessions = sessions;
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.sessions.is_empty()
|
||||
}
|
||||
|
||||
fn render_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
if self.sessions.is_empty() || width < 4 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let label = "Background terminal running:";
|
||||
let mut spans = Vec::new();
|
||||
spans.push(label.dim());
|
||||
spans.push(" ".into());
|
||||
|
||||
let visible = self.sessions.iter().take(MAX_VISIBLE_SESSIONS);
|
||||
let mut visible_count = 0usize;
|
||||
for (idx, command) in visible.enumerate() {
|
||||
if idx > 0 {
|
||||
spans.push(" · ".dim());
|
||||
}
|
||||
let truncated = truncate_text(command, MAX_SESSION_LABEL_GRAPHEMES);
|
||||
spans.push(truncated.cyan());
|
||||
visible_count += 1;
|
||||
}
|
||||
|
||||
let remaining = self.sessions.len().saturating_sub(visible_count);
|
||||
if remaining > 0 {
|
||||
spans.push(" · ".dim());
|
||||
spans.push(format!("{remaining} more running").dim());
|
||||
}
|
||||
|
||||
let indent = " ".repeat(label.len() + 1);
|
||||
let line = Line::from(spans);
|
||||
word_wrap_lines(
|
||||
std::iter::once(line),
|
||||
RtOptions::new(width as usize).subsequent_indent(Line::from(indent).dim()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for UnifiedExecFooter {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
Paragraph::new(self.render_lines(area.width)).render(area, buf);
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.render_lines(width).len() as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn desired_height_empty() {
|
||||
let footer = UnifiedExecFooter::new();
|
||||
assert_eq!(footer.desired_height(40), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_two_sessions() {
|
||||
let mut footer = UnifiedExecFooter::new();
|
||||
footer.set_sessions(vec!["echo hello".to_string(), "rg \"foo\" src".to_string()]);
|
||||
let width = 50;
|
||||
let height = footer.desired_height(width);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
footer.render(Rect::new(0, 0, width, height), &mut buf);
|
||||
assert_snapshot!("render_two_sessions", format!("{buf:?}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_more_sessions() {
|
||||
let mut footer = UnifiedExecFooter::new();
|
||||
footer.set_sessions(vec![
|
||||
"echo hello".to_string(),
|
||||
"rg \"foo\" src".to_string(),
|
||||
"cat README.md".to_string(),
|
||||
]);
|
||||
let width = 50;
|
||||
let height = footer.desired_height(width);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
footer.render(Rect::new(0, 0, width, height), &mut buf);
|
||||
assert_snapshot!("render_more_sessions", format!("{buf:?}"));
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +103,7 @@ use crate::diff_render::display_path_for;
|
|||
use crate::exec_cell::CommandOutput;
|
||||
use crate::exec_cell::ExecCell;
|
||||
use crate::exec_cell::new_active_exec_command;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::get_git_diff::get_git_diff;
|
||||
use crate::history_cell;
|
||||
use crate::history_cell::AgentMessageCell;
|
||||
|
|
@ -153,6 +154,11 @@ struct RunningCommand {
|
|||
source: ExecCommandSource,
|
||||
}
|
||||
|
||||
struct UnifiedExecSessionSummary {
|
||||
key: String,
|
||||
command_display: String,
|
||||
}
|
||||
|
||||
struct UnifiedExecWaitState {
|
||||
command_display: String,
|
||||
}
|
||||
|
|
@ -167,6 +173,20 @@ impl UnifiedExecWaitState {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_unified_exec_source(source: ExecCommandSource) -> bool {
|
||||
matches!(
|
||||
source,
|
||||
ExecCommandSource::UnifiedExecStartup | ExecCommandSource::UnifiedExecInteraction
|
||||
)
|
||||
}
|
||||
|
||||
fn is_standard_tool_call(parsed_cmd: &[ParsedCommand]) -> bool {
|
||||
!parsed_cmd.is_empty()
|
||||
&& parsed_cmd
|
||||
.iter()
|
||||
.all(|parsed| !matches!(parsed, ParsedCommand::Unknown { .. }))
|
||||
}
|
||||
|
||||
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [75.0, 90.0, 95.0];
|
||||
const NUDGE_MODEL_SLUG: &str = "gpt-5.1-codex-mini";
|
||||
const RATE_LIMIT_SWITCH_PROMPT_THRESHOLD: f64 = 90.0;
|
||||
|
|
@ -304,6 +324,7 @@ pub(crate) struct ChatWidget {
|
|||
suppressed_exec_calls: HashSet<String>,
|
||||
last_unified_wait: Option<UnifiedExecWaitState>,
|
||||
task_complete_pending: bool,
|
||||
unified_exec_sessions: Vec<UnifiedExecSessionSummary>,
|
||||
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
|
||||
// Queue of interruptive UI events deferred during an active write cycle
|
||||
interrupts: InterruptManager,
|
||||
|
|
@ -835,6 +856,12 @@ impl ChatWidget {
|
|||
|
||||
fn on_exec_command_begin(&mut self, ev: ExecCommandBeginEvent) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
if is_unified_exec_source(ev.source) {
|
||||
self.track_unified_exec_session_begin(&ev);
|
||||
if !is_standard_tool_call(&ev.parsed_cmd) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ev2 = ev.clone();
|
||||
self.defer_or_handle(|q| q.push_exec_begin(ev), |s| s.handle_exec_begin_now(ev2));
|
||||
}
|
||||
|
|
@ -846,8 +873,17 @@ impl ChatWidget {
|
|||
// TODO: Handle streaming exec output if/when implemented
|
||||
}
|
||||
|
||||
fn on_terminal_interaction(&mut self, _ev: TerminalInteractionEvent) {
|
||||
// TODO: Handle once design is ready
|
||||
fn on_terminal_interaction(&mut self, ev: TerminalInteractionEvent) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
let command_display = self
|
||||
.unified_exec_sessions
|
||||
.iter()
|
||||
.find(|session| session.key == ev.process_id)
|
||||
.map(|session| session.command_display.clone());
|
||||
self.add_to_history(history_cell::new_unified_exec_interaction(
|
||||
command_display,
|
||||
ev.stdin,
|
||||
));
|
||||
}
|
||||
|
||||
fn on_patch_apply_begin(&mut self, event: PatchApplyBeginEvent) {
|
||||
|
|
@ -875,10 +911,56 @@ impl ChatWidget {
|
|||
}
|
||||
|
||||
fn on_exec_command_end(&mut self, ev: ExecCommandEndEvent) {
|
||||
if is_unified_exec_source(ev.source) {
|
||||
self.track_unified_exec_session_end(&ev);
|
||||
if !self.bottom_pane.is_task_running() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ev2 = ev.clone();
|
||||
self.defer_or_handle(|q| q.push_exec_end(ev), |s| s.handle_exec_end_now(ev2));
|
||||
}
|
||||
|
||||
fn track_unified_exec_session_begin(&mut self, ev: &ExecCommandBeginEvent) {
|
||||
if ev.source != ExecCommandSource::UnifiedExecStartup {
|
||||
return;
|
||||
}
|
||||
let key = ev.process_id.clone().unwrap_or(ev.call_id.to_string());
|
||||
let command_display = strip_bash_lc_and_escape(&ev.command);
|
||||
if let Some(existing) = self
|
||||
.unified_exec_sessions
|
||||
.iter_mut()
|
||||
.find(|session| session.key == key)
|
||||
{
|
||||
existing.command_display = command_display;
|
||||
} else {
|
||||
self.unified_exec_sessions.push(UnifiedExecSessionSummary {
|
||||
key,
|
||||
command_display,
|
||||
});
|
||||
}
|
||||
self.sync_unified_exec_footer();
|
||||
}
|
||||
|
||||
fn track_unified_exec_session_end(&mut self, ev: &ExecCommandEndEvent) {
|
||||
let key = ev.process_id.clone().unwrap_or(ev.call_id.to_string());
|
||||
let before = self.unified_exec_sessions.len();
|
||||
self.unified_exec_sessions
|
||||
.retain(|session| session.key != key);
|
||||
if self.unified_exec_sessions.len() != before {
|
||||
self.sync_unified_exec_footer();
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_unified_exec_footer(&mut self) {
|
||||
let sessions = self
|
||||
.unified_exec_sessions
|
||||
.iter()
|
||||
.map(|session| session.command_display.clone())
|
||||
.collect();
|
||||
self.bottom_pane.set_unified_exec_sessions(sessions);
|
||||
}
|
||||
|
||||
fn on_mcp_tool_call_begin(&mut self, ev: McpToolCallBeginEvent) {
|
||||
let ev2 = ev.clone();
|
||||
self.defer_or_handle(|q| q.push_mcp_begin(ev), |s| s.handle_mcp_begin_now(ev2));
|
||||
|
|
@ -1326,6 +1408,7 @@ impl ChatWidget {
|
|||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
task_complete_pending: false,
|
||||
unified_exec_sessions: Vec::new(),
|
||||
mcp_startup_status: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
|
|
@ -1411,6 +1494,7 @@ impl ChatWidget {
|
|||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
task_complete_pending: false,
|
||||
unified_exec_sessions: Vec::new(),
|
||||
mcp_startup_status: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
|
|
|
|||
|
|
@ -389,6 +389,7 @@ fn make_chatwidget_manual(
|
|||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
task_complete_pending: false,
|
||||
unified_exec_sessions: Vec::new(),
|
||||
mcp_startup_status: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
|
|
@ -1184,6 +1185,7 @@ fn exec_end_without_begin_uses_event_command() {
|
|||
#[test]
|
||||
fn exec_history_shows_unified_exec_startup_commands() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
|
||||
chat.on_task_started();
|
||||
|
||||
let begin = begin_exec_with_source(
|
||||
&mut chat,
|
||||
|
|
@ -1207,6 +1209,46 @@ fn exec_history_shows_unified_exec_startup_commands() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_history_shows_unified_exec_tool_calls() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
|
||||
chat.on_task_started();
|
||||
|
||||
let begin = begin_exec_with_source(
|
||||
&mut chat,
|
||||
"call-startup",
|
||||
"ls",
|
||||
ExecCommandSource::UnifiedExecStartup,
|
||||
);
|
||||
end_exec(&mut chat, begin, "", "", 0);
|
||||
|
||||
let blob = active_blob(&chat);
|
||||
assert_eq!(blob, "• Explored\n └ List ls\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_exec_end_after_task_complete_is_suppressed() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
|
||||
chat.on_task_started();
|
||||
|
||||
let begin = begin_exec_with_source(
|
||||
&mut chat,
|
||||
"call-startup",
|
||||
"echo unified exec startup",
|
||||
ExecCommandSource::UnifiedExecStartup,
|
||||
);
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
chat.on_task_complete(None);
|
||||
end_exec(&mut chat, begin, "", "", 0);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
cells.is_empty(),
|
||||
"expected unified exec end after task complete to be suppressed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Selecting the custom prompt option from the review popup sends
|
||||
/// OpenReviewCustomPrompt to the app event channel.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -375,6 +375,72 @@ impl HistoryCell for PrefixedWrappedHistoryCell {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnifiedExecInteractionCell {
|
||||
command_display: Option<String>,
|
||||
stdin: String,
|
||||
}
|
||||
|
||||
impl UnifiedExecInteractionCell {
|
||||
pub(crate) fn new(command_display: Option<String>, stdin: String) -> Self {
|
||||
Self {
|
||||
command_display,
|
||||
stdin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryCell for UnifiedExecInteractionCell {
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
if width == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let wrap_width = width as usize;
|
||||
|
||||
let mut header_spans = vec!["↳ ".dim(), "Interacted with background terminal".bold()];
|
||||
if let Some(command) = &self.command_display
|
||||
&& !command.is_empty()
|
||||
{
|
||||
header_spans.push(" · ".dim());
|
||||
header_spans.push(command.clone().dim());
|
||||
}
|
||||
let header = Line::from(header_spans);
|
||||
|
||||
let mut out: Vec<Line<'static>> = Vec::new();
|
||||
let header_wrapped = word_wrap_line(&header, RtOptions::new(wrap_width));
|
||||
push_owned_lines(&header_wrapped, &mut out);
|
||||
|
||||
let input_lines: Vec<Line<'static>> = if self.stdin.is_empty() {
|
||||
vec![vec!["(waited)".dim()].into()]
|
||||
} else {
|
||||
self.stdin
|
||||
.lines()
|
||||
.map(|line| Line::from(line.to_string()))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let input_wrapped = word_wrap_lines(
|
||||
input_lines,
|
||||
RtOptions::new(wrap_width)
|
||||
.initial_indent(Line::from(" └ ".dim()))
|
||||
.subsequent_indent(Line::from(" ".dim())),
|
||||
);
|
||||
out.extend(input_wrapped);
|
||||
out
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
self.display_lines(width).len() as u16
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_unified_exec_interaction(
|
||||
command_display: Option<String>,
|
||||
stdin: String,
|
||||
) -> UnifiedExecInteractionCell {
|
||||
UnifiedExecInteractionCell::new(command_display, stdin)
|
||||
}
|
||||
|
||||
fn truncate_exec_snippet(full_cmd: &str) -> String {
|
||||
let mut snippet = match full_cmd.split_once('\n') {
|
||||
Some((first, _)) => format!("{first} ..."),
|
||||
|
|
@ -1558,6 +1624,31 @@ mod tests {
|
|||
render_lines(&cell.transcript_lines(u16::MAX))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_exec_interaction_cell_renders_input() {
|
||||
let cell =
|
||||
new_unified_exec_interaction(Some("echo hello".to_string()), "ls\npwd".to_string());
|
||||
let lines = render_transcript(&cell);
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
"↳ Interacted with background terminal · echo hello",
|
||||
" └ ls",
|
||||
" pwd",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_exec_interaction_cell_renders_wait() {
|
||||
let cell = new_unified_exec_interaction(None, String::new());
|
||||
let lines = render_transcript(&cell);
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec!["↳ Interacted with background terminal", " └ (waited)"],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tools_output_masks_sensitive_values() {
|
||||
let mut config = test_config();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue