mirror of
https://github.com/openai/codex.git
synced 2026-02-02 06:57:03 +00:00
Compare commits
1 Commits
fix-cli
...
feature/ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9518387ddb |
@@ -128,6 +128,7 @@ use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod compact;
|
||||
|
||||
@@ -770,6 +771,7 @@ impl Session {
|
||||
command_for_display,
|
||||
cwd,
|
||||
apply_patch,
|
||||
user_initiated_shell_command,
|
||||
} = exec_command_context;
|
||||
let msg = match apply_patch {
|
||||
Some(ApplyPatchCommandContext {
|
||||
@@ -792,6 +794,7 @@ impl Session {
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
user_initiated_shell_command,
|
||||
}),
|
||||
};
|
||||
let event = Event {
|
||||
@@ -1029,6 +1032,7 @@ pub(crate) struct ExecCommandContext {
|
||||
pub(crate) command_for_display: Vec<String>,
|
||||
pub(crate) cwd: PathBuf,
|
||||
pub(crate) apply_patch: Option<ApplyPatchCommandContext>,
|
||||
pub(crate) user_initiated_shell_command: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1474,6 +1478,101 @@ async fn submission_loop(
|
||||
};
|
||||
sess.send_event(event).await;
|
||||
}
|
||||
Op::RunUserShellCommand { command } => {
|
||||
// Spawn a cancellable one-off shell command task so we can process
|
||||
// further Ops (e.g., Interrupt) while it runs.
|
||||
let sess_clone = sess.clone();
|
||||
let turn_context = Arc::clone(&turn_context);
|
||||
let sub_id = sub.id.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
// Announce a running task so the UI can show a spinner and block input.
|
||||
let event = Event {
|
||||
id: sub_id.clone(),
|
||||
msg: EventMsg::TaskStarted(TaskStartedEvent {
|
||||
model_context_window: turn_context.client.get_model_context_window(),
|
||||
}),
|
||||
};
|
||||
sess_clone.send_event(event).await;
|
||||
|
||||
// Build a shell invocation in the user's default shell.
|
||||
let shell_invocation = sess_clone
|
||||
.user_shell
|
||||
// Why we pass a ["bash", "-lc", <script>] sentinel instead of the raw command:
|
||||
// - The shell adapter (core/src/shell.rs) first calls `strip_bash_lc`. When it sees this
|
||||
// exact shape it extracts <script> and then builds the correct argv for the user shell
|
||||
// (e.g., `/bin/zsh -lc "source ~/.zshrc && (<script>)"`).
|
||||
// - If we pass the whole command as a single string (e.g., ["cat Cargo.toml | wc -l"]) the
|
||||
// adapter may quote it when joining/embedding, and shells can treat the entire value as a
|
||||
// single program name or a single quoted token.
|
||||
.format_default_shell_invocation(vec![
|
||||
"bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
command.clone(),
|
||||
])
|
||||
.unwrap_or_else(|| vec![command.clone()]);
|
||||
|
||||
let params = ExecParams {
|
||||
command: shell_invocation.clone(),
|
||||
cwd: turn_context.cwd.clone(),
|
||||
timeout_ms: None,
|
||||
env: create_env(&turn_context.shell_environment_policy),
|
||||
with_escalated_permissions: None,
|
||||
justification: None,
|
||||
};
|
||||
|
||||
// Use a fresh diff tracker (no patch application expected for ! commands).
|
||||
let mut turn_diff_tracker = TurnDiffTracker::new();
|
||||
// Initiated by user, not by the model. Hence, we generate a new call_id.
|
||||
let call_id = format!("call_{}", Uuid::new_v4());
|
||||
|
||||
let exec_ctx = ExecCommandContext {
|
||||
sub_id: sub_id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
command_for_display: shell_invocation,
|
||||
cwd: params.cwd.clone(),
|
||||
apply_patch: None,
|
||||
user_initiated_shell_command: true,
|
||||
};
|
||||
|
||||
// Run without sandboxing or approval — this is a user-initiated command.
|
||||
// Output is not captured as it's sent to the TUI inside `run_exec_with_events`.
|
||||
let _ = sess_clone
|
||||
.run_exec_with_events(
|
||||
&mut turn_diff_tracker,
|
||||
exec_ctx,
|
||||
ExecInvokeArgs {
|
||||
params,
|
||||
sandbox_type: SandboxType::None,
|
||||
sandbox_policy: &turn_context.sandbox_policy,
|
||||
codex_linux_sandbox_exe: &sess_clone.codex_linux_sandbox_exe,
|
||||
stdout_stream: Some(StdoutStream {
|
||||
sub_id: sub_id.clone(),
|
||||
call_id: call_id.clone(),
|
||||
tx_event: sess_clone.tx_event.clone(),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Signal completion so the UI regains control.
|
||||
let complete = Event {
|
||||
id: sub_id.clone(),
|
||||
msg: EventMsg::TaskComplete(TaskCompleteEvent {
|
||||
last_agent_message: None,
|
||||
}),
|
||||
};
|
||||
sess_clone.send_event(complete).await;
|
||||
})
|
||||
.abort_handle();
|
||||
|
||||
// Track this as the current task so Interrupt can abort it.
|
||||
sess.set_task(AgentTask {
|
||||
sess: sess.clone(),
|
||||
sub_id: sub.id,
|
||||
handle,
|
||||
kind: AgentTaskKind::Regular,
|
||||
});
|
||||
}
|
||||
Op::Review { review_request } => {
|
||||
spawn_review_thread(
|
||||
sess.clone(),
|
||||
@@ -2813,6 +2912,7 @@ async fn handle_container_exec_with_params(
|
||||
changes: convert_apply_patch_to_protocol(&action),
|
||||
},
|
||||
),
|
||||
user_initiated_shell_command: false,
|
||||
};
|
||||
|
||||
let params = maybe_translate_shell_command(params, sess, turn_context);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Aggregates all former standalone integration tests as modules.
|
||||
|
||||
mod user_shell_cmd;
|
||||
mod cli_stream;
|
||||
mod client;
|
||||
mod compact;
|
||||
|
||||
112
codex-rs/core/tests/suite/user_shell_cmd.rs
Normal file
112
codex-rs/core/tests/suite/user_shell_cmd.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::NewConversation;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::ExecCommandEndEvent;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::TurnAbortReason;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::wait_for_event;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
|
||||
// No env overrides needed; test build hard-codes a hermetic zsh with empty rc.
|
||||
// Create a temporary working directory with a known file.
|
||||
let cwd = TempDir::new().unwrap();
|
||||
let file_name = "hello.txt";
|
||||
let file_path: PathBuf = cwd.path().join(file_name);
|
||||
let contents = "hello from bang test\n";
|
||||
fs::write(&file_path, contents).expect("write temp file");
|
||||
|
||||
// Load config and pin cwd to the temp dir so ls/cat operate there.
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let mut config = load_default_config_for_test(&codex_home);
|
||||
config.cwd = cwd.path().to_path_buf();
|
||||
|
||||
let conversation_manager =
|
||||
ConversationManager::with_auth(codex_core::CodexAuth::from_api_key("dummy"));
|
||||
let NewConversation {
|
||||
conversation: codex,
|
||||
..
|
||||
} = conversation_manager
|
||||
.new_conversation(config)
|
||||
.await
|
||||
.expect("create new conversation");
|
||||
|
||||
// 1) ls should list the file
|
||||
codex
|
||||
.submit(Op::RunUserShellCommand {
|
||||
command: "ls".to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandEnd(_))).await;
|
||||
let EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
stdout, exit_code, ..
|
||||
}) = msg
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(exit_code, 0);
|
||||
assert!(
|
||||
stdout.contains(file_name),
|
||||
"ls output should include {file_name}, got: {stdout:?}"
|
||||
);
|
||||
|
||||
// 2) cat the file should return exact contents
|
||||
codex
|
||||
.submit(Op::RunUserShellCommand {
|
||||
command: format!("cat {}", file_name),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandEnd(_))).await;
|
||||
let EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
stdout, exit_code, ..
|
||||
}) = msg
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(exit_code, 0);
|
||||
assert_eq!(stdout, contents);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn user_shell_cmd_can_be_interrupted() {
|
||||
// No env overrides needed; test build hard-codes a hermetic zsh with empty rc.
|
||||
// Set up isolated config and conversation.
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let config = load_default_config_for_test(&codex_home);
|
||||
let conversation_manager =
|
||||
ConversationManager::with_auth(codex_core::CodexAuth::from_api_key("dummy"));
|
||||
let NewConversation {
|
||||
conversation: codex,
|
||||
..
|
||||
} = conversation_manager
|
||||
.new_conversation(config)
|
||||
.await
|
||||
.expect("create new conversation");
|
||||
|
||||
// Start a long-running command and then interrupt it.
|
||||
codex
|
||||
.submit(Op::RunUserShellCommand {
|
||||
command: "sleep 5".to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait until it has started (ExecCommandBegin), then interrupt.
|
||||
let _ = wait_for_event(&codex, |ev| matches!(ev, EventMsg::ExecCommandBegin(_))).await;
|
||||
codex.submit(Op::Interrupt).await.unwrap();
|
||||
|
||||
// Expect a TurnAborted(Interrupted) notification.
|
||||
let msg = wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnAborted(_))).await;
|
||||
let EventMsg::TurnAborted(ev) = msg else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(ev.reason, TurnAbortReason::Interrupted);
|
||||
}
|
||||
@@ -278,6 +278,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd: _,
|
||||
..
|
||||
}) => {
|
||||
self.call_id_to_command.insert(
|
||||
call_id,
|
||||
|
||||
@@ -172,6 +172,16 @@ pub enum Op {
|
||||
|
||||
/// Request to shut down codex instance.
|
||||
Shutdown,
|
||||
|
||||
/// Execute a user-initiated one-off shell command (triggered by "!cmd").
|
||||
///
|
||||
/// The command string is executed using the user's default shell and may
|
||||
/// include shell syntax (pipes, redirects, etc.). Output is streamed via
|
||||
/// `ExecCommand*` events and the UI regains control upon `TaskComplete`.
|
||||
RunUserShellCommand {
|
||||
/// The raw command string after '!'
|
||||
command: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Determines the conditions under which the user is consulted to approve
|
||||
@@ -1032,6 +1042,10 @@ pub struct ExecCommandBeginEvent {
|
||||
/// The command's working directory if not the default cwd for the agent.
|
||||
pub cwd: PathBuf,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
/// True when this exec was initiated directly by the user (e.g. bang command),
|
||||
/// not by the agent/model. Defaults to false for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub user_initiated_shell_command: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
@@ -1298,4 +1312,19 @@ mod tests {
|
||||
let deserialized: ExecCommandOutputDeltaEvent = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(deserialized, event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_run_user_shell_command_op() {
|
||||
let op = Op::RunUserShellCommand {
|
||||
command: "echo hi".to_string(),
|
||||
};
|
||||
let value = serde_json::to_value(op).unwrap();
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"type": "run_user_shell_command",
|
||||
"command": "echo hi",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ use codex_protocol::mcp_protocol::ConversationId;
|
||||
struct RunningCommand {
|
||||
command: Vec<String>,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
user_initiated_shell_command: bool,
|
||||
}
|
||||
|
||||
/// Common initialization parameters shared by all `ChatWidget` constructors.
|
||||
@@ -477,9 +478,9 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn handle_exec_end_now(&mut self, ev: ExecCommandEndEvent) {
|
||||
let running = self.running_commands.remove(&ev.call_id);
|
||||
let (command, parsed) = match running {
|
||||
Some(rc) => (rc.command, rc.parsed_cmd),
|
||||
None => (vec![ev.call_id.clone()], Vec::new()),
|
||||
let (command, parsed, user_initiated_shell_command) = match running {
|
||||
Some(rc) => (rc.command, rc.parsed_cmd, rc.user_initiated_shell_command),
|
||||
None => (vec![ev.call_id.clone()], Vec::new(), false),
|
||||
};
|
||||
|
||||
if self.active_exec_cell.is_none() {
|
||||
@@ -489,6 +490,7 @@ impl ChatWidget {
|
||||
ev.call_id.clone(),
|
||||
command,
|
||||
parsed,
|
||||
user_initiated_shell_command,
|
||||
));
|
||||
}
|
||||
if let Some(cell) = self.active_exec_cell.as_mut() {
|
||||
@@ -561,6 +563,7 @@ impl ChatWidget {
|
||||
RunningCommand {
|
||||
command: ev.command.clone(),
|
||||
parsed_cmd: ev.parsed_cmd.clone(),
|
||||
user_initiated_shell_command: ev.user_initiated_shell_command,
|
||||
},
|
||||
);
|
||||
if let Some(exec) = &self.active_exec_cell {
|
||||
@@ -568,6 +571,7 @@ impl ChatWidget {
|
||||
ev.call_id.clone(),
|
||||
ev.command.clone(),
|
||||
ev.parsed_cmd.clone(),
|
||||
ev.user_initiated_shell_command,
|
||||
) {
|
||||
self.active_exec_cell = Some(new_exec);
|
||||
} else {
|
||||
@@ -577,6 +581,7 @@ impl ChatWidget {
|
||||
ev.call_id.clone(),
|
||||
ev.command.clone(),
|
||||
ev.parsed_cmd,
|
||||
ev.user_initiated_shell_command,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -584,6 +589,7 @@ impl ChatWidget {
|
||||
ev.call_id.clone(),
|
||||
ev.command.clone(),
|
||||
ev.parsed_cmd,
|
||||
ev.user_initiated_shell_command,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -959,6 +965,15 @@ impl ChatWidget {
|
||||
let UserMessage { text, image_paths } = user_message;
|
||||
let mut items: Vec<InputItem> = Vec::new();
|
||||
|
||||
// Special-case: "!cmd" executes a local shell command instead of sending to the model.
|
||||
if let Some(stripped) = text.strip_prefix('!') {
|
||||
let cmd = stripped.trim().to_string();
|
||||
if !cmd.is_empty() {
|
||||
self.submit_op(Op::RunUserShellCommand { command: cmd });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
items.push(InputItem::Text { text: text.clone() });
|
||||
}
|
||||
|
||||
@@ -399,6 +399,7 @@ fn begin_exec(chat: &mut ChatWidget, call_id: &str, raw_cmd: &str) {
|
||||
command,
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
parsed_cmd,
|
||||
user_initiated_shell_command: false,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -691,6 +692,7 @@ async fn binary_size_transcript_snapshot() {
|
||||
command: e.command,
|
||||
cwd: e.cwd,
|
||||
parsed_cmd: parsed_cmd.into_iter().map(|c| c.into()).collect(),
|
||||
user_initiated_shell_command: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1674,6 +1676,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
user_initiated_shell_command: false,
|
||||
}),
|
||||
});
|
||||
chat.handle_codex_event(Event {
|
||||
|
||||
@@ -213,6 +213,7 @@ pub(crate) struct ExecCall {
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) parsed: Vec<ParsedCommand>,
|
||||
pub(crate) output: Option<CommandOutput>,
|
||||
pub(crate) user_initiated_shell_command: bool,
|
||||
start_time: Option<Instant>,
|
||||
duration: Option<Duration>,
|
||||
}
|
||||
@@ -426,17 +427,28 @@ impl ExecCell {
|
||||
body_lines.extend(wrapped_borrowed.iter().map(|l| line_to_static(l)));
|
||||
}
|
||||
}
|
||||
if let Some(output) = call.output.as_ref()
|
||||
&& output.exit_code != 0
|
||||
{
|
||||
let out = output_lines(Some(output), false, false, false)
|
||||
.into_iter()
|
||||
.join("\n");
|
||||
if !out.trim().is_empty() {
|
||||
// Wrap the output.
|
||||
for line in out.lines() {
|
||||
let wrapped = textwrap::wrap(line, TwOptions::new(width as usize - 4));
|
||||
body_lines.extend(wrapped.into_iter().map(|l| Line::from(l.to_string().dim())));
|
||||
if let Some(output) = call.output.as_ref() {
|
||||
// ExecCell-level classification: if any call in the cell represents
|
||||
// a user shell command (ParsedCommand::Unknown), show output
|
||||
// unconditionally. Otherwise, only show on errors.
|
||||
let is_shell_exec_cell = self.calls.iter().any(|c| c.user_initiated_shell_command);
|
||||
let should_show_output = is_shell_exec_cell || output.exit_code != 0;
|
||||
if should_show_output {
|
||||
let line_limit = if is_shell_exec_cell {
|
||||
USER_SHELL_TOOL_CALL_MAX_LINES
|
||||
} else {
|
||||
TOOL_CALL_MAX_LINES
|
||||
};
|
||||
let out = output_lines(Some(output), false, false, false, line_limit)
|
||||
.into_iter()
|
||||
.join("\n");
|
||||
if !out.trim().is_empty() {
|
||||
// Wrap the output.
|
||||
for line in out.lines() {
|
||||
let wrapped = textwrap::wrap(line, TwOptions::new(width as usize - 4));
|
||||
body_lines
|
||||
.extend(wrapped.into_iter().map(|l| Line::from(l.to_string().dim())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,11 +531,13 @@ impl ExecCell {
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
user_initiated_shell_command: bool,
|
||||
) -> Option<Self> {
|
||||
let call = ExecCall {
|
||||
call_id,
|
||||
command,
|
||||
parsed,
|
||||
user_initiated_shell_command,
|
||||
output: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
@@ -566,6 +580,7 @@ impl HistoryCell for CompletedMcpToolCallWithImageOutput {
|
||||
}
|
||||
|
||||
const TOOL_CALL_MAX_LINES: usize = 5;
|
||||
const USER_SHELL_TOOL_CALL_MAX_LINES: usize = 50;
|
||||
|
||||
fn title_case(s: &str) -> String {
|
||||
if s.is_empty() {
|
||||
@@ -676,11 +691,13 @@ pub(crate) fn new_active_exec_command(
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
user_initiated_shell_command: bool,
|
||||
) -> ExecCell {
|
||||
ExecCell::new(ExecCall {
|
||||
call_id,
|
||||
command,
|
||||
parsed,
|
||||
user_initiated_shell_command,
|
||||
output: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
@@ -1174,6 +1191,7 @@ pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
TOOL_CALL_MAX_LINES,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1259,6 +1277,7 @@ fn output_lines(
|
||||
only_err: bool,
|
||||
include_angle_pipe: bool,
|
||||
include_prefix: bool,
|
||||
line_limit: usize,
|
||||
) -> Vec<Line<'static>> {
|
||||
let CommandOutput {
|
||||
exit_code,
|
||||
@@ -1274,7 +1293,7 @@ fn output_lines(
|
||||
let src = if *exit_code == 0 { stdout } else { stderr };
|
||||
let lines: Vec<&str> = src.lines().collect();
|
||||
let total = lines.len();
|
||||
let limit = TOOL_CALL_MAX_LINES;
|
||||
let limit = line_limit;
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -1397,6 +1416,7 @@ mod tests {
|
||||
},
|
||||
],
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1428,6 +1448,7 @@ mod tests {
|
||||
cmd: "rg shimmer_spans".into(),
|
||||
}],
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1451,6 +1472,7 @@ mod tests {
|
||||
name: "shimmer.rs".into(),
|
||||
cmd: "cat shimmer.rs".into(),
|
||||
}],
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
cell.complete_call(
|
||||
@@ -1472,6 +1494,7 @@ mod tests {
|
||||
name: "status_indicator_widget.rs".into(),
|
||||
cmd: "cat status_indicator_widget.rs".into(),
|
||||
}],
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
cell.complete_call(
|
||||
@@ -1510,6 +1533,7 @@ mod tests {
|
||||
},
|
||||
],
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1538,6 +1562,7 @@ mod tests {
|
||||
command: vec!["bash".into(), "-lc".into(), cmd],
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1568,6 +1593,7 @@ mod tests {
|
||||
command: vec!["echo".into(), "ok".into()],
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1596,6 +1622,7 @@ mod tests {
|
||||
command: vec!["bash".into(), "-lc".into(), long],
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1623,6 +1650,7 @@ mod tests {
|
||||
command: vec!["bash".into(), "-lc".into(), cmd],
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1651,6 +1679,7 @@ mod tests {
|
||||
command: vec!["bash".into(), "-lc".into(), cmd],
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1679,6 +1708,7 @@ mod tests {
|
||||
command: vec!["bash".into(), "-lc".into(), "seq 1 10 1>&2 && false".into()],
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
@@ -1725,6 +1755,7 @@ mod tests {
|
||||
command: vec!["bash".into(), "-lc".into(), long_cmd.to_string()],
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
user_initiated_shell_command: false,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user