mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-07 16:44:50 +08:00
feat: b5-stdin-pipe — batch 5 upstream parity
This commit is contained in:
@@ -13,7 +13,7 @@ mod render;
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, IsTerminal, Read, Write};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -112,6 +112,45 @@ Run `claw --help` for usage."
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read piped stdin content when stdin is not a terminal.
|
||||||
|
///
|
||||||
|
/// Returns `None` when stdin is attached to a terminal (interactive REPL use),
|
||||||
|
/// when reading fails, or when the piped content is empty after trimming.
|
||||||
|
/// Returns `Some(raw_content)` when a pipe delivered non-empty content.
|
||||||
|
fn read_piped_stdin() -> Option<String> {
|
||||||
|
if io::stdin().is_terminal() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut buffer = String::new();
|
||||||
|
if io::stdin().read_to_string(&mut buffer).is_err() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if buffer.trim().is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge a piped stdin payload into a prompt argument.
|
||||||
|
///
|
||||||
|
/// When `stdin_content` is `None` or empty after trimming, the prompt is
|
||||||
|
/// returned unchanged. Otherwise the trimmed stdin content is appended to the
|
||||||
|
/// prompt separated by a blank line so the model sees the prompt first and the
|
||||||
|
/// piped context immediately after it.
|
||||||
|
fn merge_prompt_with_stdin(prompt: &str, stdin_content: Option<&str>) -> String {
|
||||||
|
let Some(raw) = stdin_content else {
|
||||||
|
return prompt.to_string();
|
||||||
|
};
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return prompt.to_string();
|
||||||
|
}
|
||||||
|
if prompt.is_empty() {
|
||||||
|
return trimmed.to_string();
|
||||||
|
}
|
||||||
|
format!("{prompt}\n\n{trimmed}")
|
||||||
|
}
|
||||||
|
|
||||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args: Vec<String> = env::args().skip(1).collect();
|
let args: Vec<String> = env::args().skip(1).collect();
|
||||||
match parse_args(&args)? {
|
match parse_args(&args)? {
|
||||||
@@ -157,9 +196,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
output_format,
|
output_format,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
compact,
|
} => {
|
||||||
} => LiveCli::new(model, true, allowed_tools, permission_mode)?
|
let stdin_context = read_piped_stdin();
|
||||||
.run_turn_with_output(&prompt, output_format, compact)?,
|
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
|
||||||
|
LiveCli::new(model, true, allowed_tools, permission_mode)?
|
||||||
|
.run_turn_with_output(&effective_prompt, output_format)?;
|
||||||
|
}
|
||||||
CliAction::Login { output_format } => run_login(output_format)?,
|
CliAction::Login { output_format } => run_login(output_format)?,
|
||||||
CliAction::Logout { output_format } => run_logout(output_format)?,
|
CliAction::Logout { output_format } => run_logout(output_format)?,
|
||||||
CliAction::Doctor { output_format } => run_doctor(output_format)?,
|
CliAction::Doctor { output_format } => run_doctor(output_format)?,
|
||||||
@@ -7417,7 +7459,7 @@ mod tests {
|
|||||||
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
|
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
|
||||||
format_ultraplan_report, format_unknown_slash_command,
|
format_ultraplan_report, format_unknown_slash_command,
|
||||||
format_unknown_slash_command_message, format_user_visible_api_error,
|
format_unknown_slash_command_message, format_user_visible_api_error,
|
||||||
normalize_permission_mode, parse_args, parse_export_args, parse_git_status_branch,
|
merge_prompt_with_stdin, normalize_permission_mode, parse_args, parse_git_status_branch,
|
||||||
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
||||||
print_help_to, push_output_block, render_config_report, render_diff_report,
|
print_help_to, push_output_block, render_config_report, render_diff_report,
|
||||||
render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage,
|
render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage,
|
||||||
@@ -7917,6 +7959,70 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_prompt_with_stdin_returns_prompt_unchanged_when_no_pipe() {
|
||||||
|
// given
|
||||||
|
let prompt = "Review this";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let merged = merge_prompt_with_stdin(prompt, None);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(merged, "Review this");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_prompt_with_stdin_ignores_whitespace_only_pipe() {
|
||||||
|
// given
|
||||||
|
let prompt = "Review this";
|
||||||
|
let piped = " \n\t\n ";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let merged = merge_prompt_with_stdin(prompt, Some(piped));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(merged, "Review this");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_prompt_with_stdin_appends_piped_content_as_context() {
|
||||||
|
// given
|
||||||
|
let prompt = "Review this";
|
||||||
|
let piped = "fn main() { println!(\"hi\"); }\n";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let merged = merge_prompt_with_stdin(prompt, Some(piped));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(merged, "Review this\n\nfn main() { println!(\"hi\"); }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_prompt_with_stdin_trims_surrounding_whitespace_on_pipe() {
|
||||||
|
// given
|
||||||
|
let prompt = "Summarize";
|
||||||
|
let piped = "\n\n some notes \n\n";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let merged = merge_prompt_with_stdin(prompt, Some(piped));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(merged, "Summarize\n\nsome notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_prompt_with_stdin_returns_pipe_when_prompt_is_empty() {
|
||||||
|
// given
|
||||||
|
let prompt = "";
|
||||||
|
let piped = "standalone body";
|
||||||
|
|
||||||
|
// when
|
||||||
|
let merged = merge_prompt_with_stdin(prompt, Some(piped));
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(merged, "standalone body");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_bare_prompt_and_json_output_flag() {
|
fn parses_bare_prompt_and_json_output_flag() {
|
||||||
let _guard = env_lock();
|
let _guard = env_lock();
|
||||||
|
|||||||
Reference in New Issue
Block a user