@@ -22,9 +22,9 @@ use commands::{
use compat_harness ::{ extract_manifest , UpstreamPaths } ;
use compat_harness ::{ extract_manifest , UpstreamPaths } ;
use render ::{ Spinner , TerminalRenderer } ;
use render ::{ Spinner , TerminalRenderer } ;
use runtime ::{
use runtime ::{
clear_oauth_credentials , format_usd , generate_pkce_pair , generate_state , load_system_prompt ,
clear_oauth_credentials , generate_pkce_pair , generate_state , load_system_prompt ,
parse_oauth_callback_request_target , pricing_for_model , save_oauth_credentials , ApiClient ,
parse_oauth_callback_request_target , save_oauth_credentials , ApiClient , ApiRequest ,
ApiRequest , AssistantEvent, CompactionConfig , ConfigLoader , ConfigSource , ContentBlock ,
AssistantEvent , CompactionConfig , ConfigLoader , ConfigSource , ContentBlock ,
ConversationMessage , ConversationRuntime , MessageRole , OAuthAuthorizationRequest ,
ConversationMessage , ConversationRuntime , MessageRole , OAuthAuthorizationRequest ,
OAuthTokenExchangeRequest , PermissionMode , PermissionPolicy , ProjectContext , RuntimeError ,
OAuthTokenExchangeRequest , PermissionMode , PermissionPolicy , ProjectContext , RuntimeError ,
Session , TokenUsage , ToolError , ToolExecutor , UsageTracker ,
Session , TokenUsage , ToolError , ToolExecutor , UsageTracker ,
@@ -36,7 +36,6 @@ const DEFAULT_MODEL: &str = "claude-sonnet-4-20250514";
const DEFAULT_MAX_TOKENS : u32 = 32 ;
const DEFAULT_MAX_TOKENS : u32 = 32 ;
const DEFAULT_DATE : & str = " 2026-03-31 " ;
const DEFAULT_DATE : & str = " 2026-03-31 " ;
const DEFAULT_OAUTH_CALLBACK_PORT : u16 = 4545 ;
const DEFAULT_OAUTH_CALLBACK_PORT : u16 = 4545 ;
const COST_WARNING_FRACTION : f64 = 0.8 ;
const VERSION : & str = env! ( " CARGO_PKG_VERSION " ) ;
const VERSION : & str = env! ( " CARGO_PKG_VERSION " ) ;
const BUILD_TARGET : Option < & str > = option_env! ( " TARGET " ) ;
const BUILD_TARGET : Option < & str > = option_env! ( " TARGET " ) ;
const GIT_SHA : Option < & str > = option_env! ( " GIT_SHA " ) ;
const GIT_SHA : Option < & str > = option_env! ( " GIT_SHA " ) ;
@@ -71,8 +70,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
output_format ,
output_format ,
allowed_tools ,
allowed_tools ,
permission_mode ,
permission_mode ,
max_cost_usd ,
} = > LiveCli ::new ( model , false , allowed_tools , permission_mode ) ?
} = > LiveCli ::new ( model , false , allowed_tools , permission_mode , max_cost_usd ) ?
. run_turn_with_output ( & prompt , output_format ) ? ,
. run_turn_with_output ( & prompt , output_format ) ? ,
CliAction ::Login = > run_login ( ) ? ,
CliAction ::Login = > run_login ( ) ? ,
CliAction ::Logout = > run_logout ( ) ? ,
CliAction ::Logout = > run_logout ( ) ? ,
@@ -80,14 +78,13 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
model ,
model ,
allowed_tools ,
allowed_tools ,
permission_mode ,
permission_mode ,
max_cost_usd ,
} = > run_repl ( model , allowed_tools , permission_mode ) ? ,
} = > run_repl ( model , allowed_tools , permission_mode , max_cost_usd ) ? ,
CliAction ::Help = > print_help ( ) ,
CliAction ::Help = > print_help ( ) ,
}
}
Ok ( ( ) )
Ok ( ( ) )
}
}
#[ derive(Debug, Clone, PartialEq) ]
#[ derive(Debug, Clone, PartialEq, Eq ) ]
enum CliAction {
enum CliAction {
DumpManifests ,
DumpManifests ,
BootstrapPlan ,
BootstrapPlan ,
@@ -106,7 +103,6 @@ enum CliAction {
output_format : CliOutputFormat ,
output_format : CliOutputFormat ,
allowed_tools : Option < AllowedToolSet > ,
allowed_tools : Option < AllowedToolSet > ,
permission_mode : PermissionMode ,
permission_mode : PermissionMode ,
max_cost_usd : Option < f64 > ,
} ,
} ,
Login ,
Login ,
Logout ,
Logout ,
@@ -114,7 +110,6 @@ enum CliAction {
model : String ,
model : String ,
allowed_tools : Option < AllowedToolSet > ,
allowed_tools : Option < AllowedToolSet > ,
permission_mode : PermissionMode ,
permission_mode : PermissionMode ,
max_cost_usd : Option < f64 > ,
} ,
} ,
// prompt-mode formatting is only supported for non-interactive runs
// prompt-mode formatting is only supported for non-interactive runs
Help ,
Help ,
@@ -144,7 +139,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
let mut output_format = CliOutputFormat ::Text ;
let mut output_format = CliOutputFormat ::Text ;
let mut permission_mode = default_permission_mode ( ) ;
let mut permission_mode = default_permission_mode ( ) ;
let mut wants_version = false ;
let mut wants_version = false ;
let mut max_cost_usd : Option < f64 > = None ;
let mut allowed_tool_values = Vec ::new ( ) ;
let mut allowed_tool_values = Vec ::new ( ) ;
let mut rest = Vec ::new ( ) ;
let mut rest = Vec ::new ( ) ;
let mut index = 0 ;
let mut index = 0 ;
@@ -180,13 +174,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
permission_mode = parse_permission_mode_arg ( value ) ? ;
permission_mode = parse_permission_mode_arg ( value ) ? ;
index + = 2 ;
index + = 2 ;
}
}
" --max-cost " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --max-cost " . to_string ( ) ) ? ;
max_cost_usd = Some ( parse_max_cost_arg ( value ) ? ) ;
index + = 2 ;
}
flag if flag . starts_with ( " --output-format= " ) = > {
flag if flag . starts_with ( " --output-format= " ) = > {
output_format = CliOutputFormat ::parse ( & flag [ 16 .. ] ) ? ;
output_format = CliOutputFormat ::parse ( & flag [ 16 .. ] ) ? ;
index + = 1 ;
index + = 1 ;
@@ -195,10 +182,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
permission_mode = parse_permission_mode_arg ( & flag [ 18 .. ] ) ? ;
permission_mode = parse_permission_mode_arg ( & flag [ 18 .. ] ) ? ;
index + = 1 ;
index + = 1 ;
}
}
flag if flag . starts_with ( " --max-cost= " ) = > {
max_cost_usd = Some ( parse_max_cost_arg ( & flag [ 11 .. ] ) ? ) ;
index + = 1 ;
}
" --allowedTools " | " --allowed-tools " = > {
" --allowedTools " | " --allowed-tools " = > {
let value = args
let value = args
. get ( index + 1 )
. get ( index + 1 )
@@ -232,7 +215,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
model ,
model ,
allowed_tools ,
allowed_tools ,
permission_mode ,
permission_mode ,
max_cost_usd ,
} ) ;
} ) ;
}
}
if matches! ( rest . first ( ) . map ( String ::as_str ) , Some ( " --help " | " -h " ) ) {
if matches! ( rest . first ( ) . map ( String ::as_str ) , Some ( " --help " | " -h " ) ) {
@@ -259,7 +241,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
output_format ,
output_format ,
allowed_tools ,
allowed_tools ,
permission_mode ,
permission_mode ,
max_cost_usd ,
} )
} )
}
}
other if ! other . starts_with ( '/' ) = > Ok ( CliAction ::Prompt {
other if ! other . starts_with ( '/' ) = > Ok ( CliAction ::Prompt {
@@ -268,7 +249,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
output_format ,
output_format ,
allowed_tools ,
allowed_tools ,
permission_mode ,
permission_mode ,
max_cost_usd ,
} ) ,
} ) ,
other = > Err ( format! ( " unknown subcommand: {other} " ) ) ,
other = > Err ( format! ( " unknown subcommand: {other} " ) ) ,
}
}
@@ -332,18 +312,6 @@ fn parse_permission_mode_arg(value: &str) -> Result<PermissionMode, String> {
. map ( permission_mode_from_label )
. map ( permission_mode_from_label )
}
}
fn parse_max_cost_arg ( value : & str ) -> Result < f64 , String > {
let parsed = value
. parse ::< f64 > ( )
. map_err ( | _ | format! ( " invalid value for --max-cost: {value} " ) ) ? ;
if ! parsed . is_finite ( ) | | parsed < = 0.0 {
return Err ( format! (
" --max-cost must be a positive finite USD amount: {value} "
) ) ;
}
Ok ( parsed )
}
fn permission_mode_from_label ( mode : & str ) -> PermissionMode {
fn permission_mode_from_label ( mode : & str ) -> PermissionMode {
match mode {
match mode {
" read-only " = > PermissionMode ::ReadOnly ,
" read-only " = > PermissionMode ::ReadOnly ,
@@ -710,78 +678,22 @@ fn format_permissions_switch_report(previous: &str, next: &str) -> String {
)
)
}
}
fn format_cost_report ( model : & str , usage : TokenUsage , max_cost_usd : Option < f64 > ) -> String {
fn format_cost_report ( usage : TokenUsage ) -> String {
let estimate = usage_cost_estimate ( model , usage ) ;
format! (
format! (
" Cost
" Cost
Model {model}
Input tokens {}
Input tokens {}
Output tokens {}
Output tokens {}
Cache create {}
Cache create {}
Cache read {}
Cache read {}
Total tokens {}
Total tokens {} " ,
Input cost {}
Output cost {}
Cache create usd {}
Cache read usd {}
Estimated cost {}
Budget {} " ,
usage . input_tokens ,
usage . input_tokens ,
usage . output_tokens ,
usage . output_tokens ,
usage . cache_creation_input_tokens ,
usage . cache_creation_input_tokens ,
usage . cache_read_input_tokens ,
usage . cache_read_input_tokens ,
usage . total_tokens ( ) ,
usage . total_tokens ( ) ,
format_usd ( estimate . input_cost_usd ) ,
format_usd ( estimate . output_cost_usd ) ,
format_usd ( estimate . cache_creation_cost_usd ) ,
format_usd ( estimate . cache_read_cost_usd ) ,
format_usd ( estimate . total_cost_usd ( ) ) ,
format_budget_line ( estimate . total_cost_usd ( ) , max_cost_usd ) ,
)
)
}
}
fn usage_cost_estimate ( model : & str , usage : TokenUsage ) -> runtime ::UsageCostEstimate {
pricing_for_model ( model ) . map_or_else (
| | usage . estimate_cost_usd ( ) ,
| pricing | usage . estimate_cost_usd_with_pricing ( pricing ) ,
)
}
fn usage_cost_total ( model : & str , usage : TokenUsage ) -> f64 {
usage_cost_estimate ( model , usage ) . total_cost_usd ( )
}
fn format_budget_line ( cost_usd : f64 , max_cost_usd : Option < f64 > ) -> String {
match max_cost_usd {
Some ( limit ) = > format! ( " {} / {} " , format_usd ( cost_usd ) , format_usd ( limit ) ) ,
None = > format! ( " {} (unlimited) " , format_usd ( cost_usd ) ) ,
}
}
fn budget_notice_message (
model : & str ,
usage : TokenUsage ,
max_cost_usd : Option < f64 > ,
) -> Option < String > {
let limit = max_cost_usd ? ;
let cost = usage_cost_total ( model , usage ) ;
if cost > = limit {
Some ( format! (
" cost budget exceeded: cumulative= {} budget= {} " ,
format_usd ( cost ) ,
format_usd ( limit )
) )
} else if cost > = limit * COST_WARNING_FRACTION {
Some ( format! (
" approaching cost budget: cumulative= {} budget= {} " ,
format_usd ( cost ) ,
format_usd ( limit )
) )
} else {
None
}
}
fn format_resume_report ( session_path : & str , message_count : usize , turns : u32 ) -> String {
fn format_resume_report ( session_path : & str , message_count : usize , turns : u32 ) -> String {
format! (
format! (
" Session resumed
" Session resumed
@@ -830,27 +742,61 @@ fn format_compact_report(removed: usize, resulting_messages: usize, skipped: boo
}
}
fn parse_git_status_metadata ( status : Option < & str > ) -> ( Option < PathBuf > , Option < String > ) {
fn parse_git_status_metadata ( status : Option < & str > ) -> ( Option < PathBuf > , Option < String > ) {
let Some ( status ) = status else {
parse_git_status_metadata_for (
return ( None , None ) ;
& env ::current_dir ( ) . unwrap_or_else ( | _ | PathBuf ::from ( " . " ) ) ,
} ;
status ,
let branch = status . lines ( ) . next ( ) . and_then ( | line | {
)
line . strip_prefix ( " ## " )
. map ( | line | {
line . split ( [ '.' , ' ' ] )
. next ( )
. unwrap_or_default ( )
. to_string ( )
} )
. filter ( | value | ! value . is_empty ( ) )
} ) ;
let project_root = find_git_root ( ) . ok ( ) ;
( project_root , branch )
}
}
fn find_git_root ( ) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
fn parse_git_status_branch ( status : Option < & str > ) -> Option < String > {
let status = status ? ;
let first_line = status . lines ( ) . next ( ) ? ;
let line = first_line . strip_prefix ( " ## " ) ? ;
if line . starts_with ( " HEAD " ) {
return Some ( " detached HEAD " . to_string ( ) ) ;
}
let branch = line . split ( [ '.' , ' ' ] ) . next ( ) . unwrap_or_default ( ) . trim ( ) ;
if branch . is_empty ( ) {
None
} else {
Some ( branch . to_string ( ) )
}
}
fn resolve_git_branch_for ( cwd : & Path ) -> Option < String > {
let branch = run_git_capture_in ( cwd , & [ " branch " , " --show-current " ] ) ? ;
let branch = branch . trim ( ) ;
if ! branch . is_empty ( ) {
return Some ( branch . to_string ( ) ) ;
}
let fallback = run_git_capture_in ( cwd , & [ " rev-parse " , " --abbrev-ref " , " HEAD " ] ) ? ;
let fallback = fallback . trim ( ) ;
if fallback . is_empty ( ) {
None
} else if fallback = = " HEAD " {
Some ( " detached HEAD " . to_string ( ) )
} else {
Some ( fallback . to_string ( ) )
}
}
fn run_git_capture_in ( cwd : & Path , args : & [ & str ] ) -> Option < String > {
let output = std ::process ::Command ::new ( " git " )
. args ( args )
. current_dir ( cwd )
. output ( )
. ok ( ) ? ;
if ! output . status . success ( ) {
return None ;
}
String ::from_utf8 ( output . stdout ) . ok ( )
}
fn find_git_root_in ( cwd : & Path ) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let output = std ::process ::Command ::new ( " git " )
let output = std ::process ::Command ::new ( " git " )
. args ( [ " rev-parse " , " --show-toplevel " ] )
. args ( [ " rev-parse " , " --show-toplevel " ] )
. current_dir ( env ::current_dir ( ) ? )
. current_dir ( cwd )
. output ( ) ? ;
. output ( ) ? ;
if ! output . status . success ( ) {
if ! output . status . success ( ) {
return Err ( " not a git repository " . into ( ) ) ;
return Err ( " not a git repository " . into ( ) ) ;
@@ -862,6 +808,15 @@ fn find_git_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
Ok ( PathBuf ::from ( path ) )
Ok ( PathBuf ::from ( path ) )
}
}
fn parse_git_status_metadata_for (
cwd : & Path ,
status : Option < & str > ,
) -> ( Option < PathBuf > , Option < String > ) {
let branch = resolve_git_branch_for ( cwd ) . or_else ( | | parse_git_status_branch ( status ) ) ;
let project_root = find_git_root_in ( cwd ) . ok ( ) ;
( project_root , branch )
}
#[ allow(clippy::too_many_lines) ]
#[ allow(clippy::too_many_lines) ]
fn run_resume_command (
fn run_resume_command (
session_path : & Path ,
session_path : & Path ,
@@ -925,7 +880,6 @@ fn run_resume_command(
} ,
} ,
default_permission_mode ( ) . as_str ( ) ,
default_permission_mode ( ) . as_str ( ) ,
& status_context ( Some ( session_path ) ) ? ,
& status_context ( Some ( session_path ) ) ? ,
None ,
) ) ,
) ) ,
} )
} )
}
}
@@ -933,7 +887,7 @@ fn run_resume_command(
let usage = UsageTracker ::from_session ( session ) . cumulative_usage ( ) ;
let usage = UsageTracker ::from_session ( session ) . cumulative_usage ( ) ;
Ok ( ResumeCommandOutcome {
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
session : session . clone ( ) ,
message : Some ( format_cost_report ( " restored-session " , usage , Non e) ) ,
message : Some ( format_cost_report ( usag e) ) ,
} )
} )
}
}
SlashCommand ::Config { section } = > Ok ( ResumeCommandOutcome {
SlashCommand ::Config { section } = > Ok ( ResumeCommandOutcome {
@@ -950,7 +904,9 @@ fn run_resume_command(
} ) ,
} ) ,
SlashCommand ::Diff = > Ok ( ResumeCommandOutcome {
SlashCommand ::Diff = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
session : session . clone ( ) ,
message : Some ( render_diff_report () ? ) ,
message : Some ( render_diff_report_for (
session_path . parent ( ) . unwrap_or_else ( | | Path ::new ( " . " ) ) ,
) ? ) ,
} ) ,
} ) ,
SlashCommand ::Version = > Ok ( ResumeCommandOutcome {
SlashCommand ::Version = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
session : session . clone ( ) ,
@@ -980,9 +936,8 @@ fn run_repl(
model : String ,
model : String ,
allowed_tools : Option < AllowedToolSet > ,
allowed_tools : Option < AllowedToolSet > ,
permission_mode : PermissionMode ,
permission_mode : PermissionMode ,
max_cost_usd : Option < f64 > ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let mut cli = LiveCli ::new ( model , true , allowed_tools , permission_mode , max_cost_usd )? ;
let mut cli = LiveCli ::new ( model , true , allowed_tools , permission_mode ) ? ;
let mut editor = input ::LineEditor ::new ( " › " , slash_command_completion_candidates ( ) ) ;
let mut editor = input ::LineEditor ::new ( " › " , slash_command_completion_candidates ( ) ) ;
println! ( " {} " , cli . startup_banner ( ) ) ;
println! ( " {} " , cli . startup_banner ( ) ) ;
@@ -1035,7 +990,6 @@ struct LiveCli {
model : String ,
model : String ,
allowed_tools : Option < AllowedToolSet > ,
allowed_tools : Option < AllowedToolSet > ,
permission_mode : PermissionMode ,
permission_mode : PermissionMode ,
max_cost_usd : Option < f64 > ,
system_prompt : Vec < String > ,
system_prompt : Vec < String > ,
runtime : ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > ,
runtime : ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > ,
session : SessionHandle ,
session : SessionHandle ,
@@ -1047,7 +1001,6 @@ impl LiveCli {
enable_tools : bool ,
enable_tools : bool ,
allowed_tools : Option < AllowedToolSet > ,
allowed_tools : Option < AllowedToolSet > ,
permission_mode : PermissionMode ,
permission_mode : PermissionMode ,
max_cost_usd : Option < f64 > ,
) -> Result < Self , Box < dyn std ::error ::Error > > {
) -> Result < Self , Box < dyn std ::error ::Error > > {
let system_prompt = build_system_prompt ( ) ? ;
let system_prompt = build_system_prompt ( ) ? ;
let session = create_managed_session_handle ( ) ? ;
let session = create_managed_session_handle ( ) ? ;
@@ -1063,7 +1016,6 @@ impl LiveCli {
model ,
model ,
allowed_tools ,
allowed_tools ,
permission_mode ,
permission_mode ,
max_cost_usd ,
system_prompt ,
system_prompt ,
runtime ,
runtime ,
session ,
session ,
@@ -1074,10 +1026,9 @@ impl LiveCli {
fn startup_banner ( & self ) -> String {
fn startup_banner ( & self ) -> String {
format! (
format! (
" Rusty Claude CLI \n Model {} \n Permission mode {} \n Cost budget {} \n Working directory {} \n Session {} \n \n Type /help for commands. Shift+Enter or Ctrl+J inserts a newline. " ,
" Rusty Claude CLI \n Model {} \n Permission mode {} \n Working directory {} \n Session {} \n \n Type /help for commands. Shift+Enter or Ctrl+J inserts a newline. " ,
self . model ,
self . model ,
self . permission_mode . as_str ( ) ,
self . permission_mode . as_str ( ) ,
self . max_cost_usd . map_or_else ( | | " none " . to_string ( ) , format_usd ) ,
env ::current_dir ( ) . map_or_else (
env ::current_dir ( ) . map_or_else (
| _ | " <unknown> " . to_string ( ) ,
| _ | " <unknown> " . to_string ( ) ,
| path | path . display ( ) . to_string ( ) ,
| path | path . display ( ) . to_string ( ) ,
@@ -1087,7 +1038,6 @@ impl LiveCli {
}
}
fn run_turn ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
fn run_turn ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
self . enforce_budget_before_turn ( ) ? ;
let mut spinner = Spinner ::new ( ) ;
let mut spinner = Spinner ::new ( ) ;
let mut stdout = io ::stdout ( ) ;
let mut stdout = io ::stdout ( ) ;
spinner . tick (
spinner . tick (
@@ -1098,14 +1048,13 @@ impl LiveCli {
let mut permission_prompter = CliPermissionPrompter ::new ( self . permission_mode ) ;
let mut permission_prompter = CliPermissionPrompter ::new ( self . permission_mode ) ;
let result = self . runtime . run_turn ( input , Some ( & mut permission_prompter ) ) ;
let result = self . runtime . run_turn ( input , Some ( & mut permission_prompter ) ) ;
match result {
match result {
Ok ( summary ) = > {
Ok ( _ ) = > {
spinner . finish (
spinner . finish (
" Claude response complete " ,
" Claude response complete " ,
TerminalRenderer ::new ( ) . color_theme ( ) ,
TerminalRenderer ::new ( ) . color_theme ( ) ,
& mut stdout ,
& mut stdout ,
) ? ;
) ? ;
println! ( ) ;
println! ( ) ;
self . print_budget_notice ( summary . usage ) ;
self . persist_session ( ) ? ;
self . persist_session ( ) ? ;
Ok ( ( ) )
Ok ( ( ) )
}
}
@@ -1132,7 +1081,6 @@ impl LiveCli {
}
}
fn run_prompt_json ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
fn run_prompt_json ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
self . enforce_budget_before_turn ( ) ? ;
let client = AnthropicClient ::from_auth ( resolve_cli_auth_source ( ) ? ) ;
let client = AnthropicClient ::from_auth ( resolve_cli_auth_source ( ) ? ) ;
let request = MessageRequest {
let request = MessageRequest {
model : self . model . clone ( ) ,
model : self . model . clone ( ) ,
@@ -1159,27 +1107,17 @@ impl LiveCli {
} )
} )
. collect ::< Vec < _ > > ( )
. collect ::< Vec < _ > > ( )
. join ( " " ) ;
. join ( " " ) ;
let usage = TokenUsage {
input_tokens : response . usage . input_tokens ,
output_tokens : response . usage . output_tokens ,
cache_creation_input_tokens : response . usage . cache_creation_input_tokens ,
cache_read_input_tokens : response . usage . cache_read_input_tokens ,
} ;
println! (
println! (
" {} " ,
" {} " ,
json! ( {
json! ( {
" message " : text ,
" message " : text ,
" model " : self . model ,
" model " : self . model ,
" usage " : {
" usage " : {
" input_tokens " : usage . input_tokens ,
" input_tokens " : response . usage. input_tokens ,
" output_tokens " : usage . output_tokens ,
" output_tokens " : response . usage. output_tokens ,
" cache_creation_input_tokens " : usage . cache_creation_input_tokens ,
" cache_creation_input_tokens " : response . usage. cache_creation_input_tokens ,
" cache_read_input_tokens " : usage . cache_read_input_tokens ,
" cache_read_input_tokens " : response . usage. cache_read_input_tokens ,
} ,
}
" cost_usd " : usage_cost_total ( & self . model , usage ) ,
" cumulative_cost_usd " : usage_cost_total ( & self . model , usage ) ,
" max_cost_usd " : self . max_cost_usd ,
" budget_warning " : budget_notice_message ( & self . model , usage , self . max_cost_usd ) ,
} )
} )
) ;
) ;
Ok ( ( ) )
Ok ( ( ) )
@@ -1249,28 +1187,6 @@ impl LiveCli {
Ok ( ( ) )
Ok ( ( ) )
}
}
fn enforce_budget_before_turn ( & self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let Some ( limit ) = self . max_cost_usd else {
return Ok ( ( ) ) ;
} ;
let cost = usage_cost_total ( & self . model , self . runtime . usage ( ) . cumulative_usage ( ) ) ;
if cost > = limit {
return Err ( format! (
" cost budget exceeded before starting turn: cumulative= {} budget= {} " ,
format_usd ( cost ) ,
format_usd ( limit )
)
. into ( ) ) ;
}
Ok ( ( ) )
}
fn print_budget_notice ( & self , usage : TokenUsage ) {
if let Some ( message ) = budget_notice_message ( & self . model , usage , self . max_cost_usd ) {
eprintln! ( " warning: {message} " ) ;
}
}
fn print_status ( & self ) {
fn print_status ( & self ) {
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
let latest = self . runtime . usage ( ) . current_turn_usage ( ) ;
let latest = self . runtime . usage ( ) . current_turn_usage ( ) ;
@@ -1287,7 +1203,6 @@ impl LiveCli {
} ,
} ,
self . permission_mode . as_str ( ) ,
self . permission_mode . as_str ( ) ,
& status_context ( Some ( & self . session . path ) ) . expect ( " status context should load " ) ,
& status_context ( Some ( & self . session . path ) ) . expect ( " status context should load " ) ,
self . max_cost_usd ,
)
)
) ;
) ;
}
}
@@ -1405,10 +1320,7 @@ impl LiveCli {
fn print_cost ( & self ) {
fn print_cost ( & self ) {
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
println! (
println! ( " {} " , format_cost_report ( cumulative ) ) ;
" {} " ,
format_cost_report ( & self . model , cumulative , self . max_cost_usd )
) ;
}
}
fn resume_session (
fn resume_session (
@@ -1686,10 +1598,7 @@ fn format_status_report(
usage : StatusUsage ,
usage : StatusUsage ,
permission_mode : & str ,
permission_mode : & str ,
context : & StatusContext ,
context : & StatusContext ,
max_cost_usd : Option < f64 > ,
) -> String {
) -> String {
let latest_cost = usage_cost_total ( model , usage . latest ) ;
let cumulative_cost = usage_cost_total ( model , usage . cumulative ) ;
[
[
format! (
format! (
" Status
" Status
@@ -1697,27 +1606,19 @@ fn format_status_report(
Permission mode {permission_mode}
Permission mode {permission_mode}
Messages {}
Messages {}
Turns {}
Turns {}
Estimated tokens {}
Estimated tokens {} " ,
Cost budget {} " ,
usage . message_count , usage . turns , usage . estimated_tokens ,
usage . message_count ,
usage . turns ,
usage . estimated_tokens ,
format_budget_line ( cumulative_cost , max_cost_usd ) ,
) ,
) ,
format! (
format! (
" Usage
" Usage
Latest total {}
Latest total {}
Latest cost {}
Cumulative input {}
Cumulative input {}
Cumulative output {}
Cumulative output {}
Cumulative total {}
Cumulative total {} " ,
Cumulative cost {} " ,
usage . latest . total_tokens ( ) ,
usage . latest . total_tokens ( ) ,
format_usd ( latest_cost ) ,
usage . cumulative . input_tokens ,
usage . cumulative . input_tokens ,
usage . cumulative . output_tokens ,
usage . cumulative . output_tokens ,
usage . cumulative . total_tokens ( ) ,
usage . cumulative . total_tokens ( ) ,
format_usd ( cumulative_cost ) ,
) ,
) ,
format! (
format! (
" Workspace
" Workspace
@@ -1939,22 +1840,43 @@ fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
}
}
fn render_diff_report ( ) -> Result < String , Box < dyn std ::error ::Error > > {
fn render_diff_report ( ) -> Result < String , Box < dyn std ::error ::Error > > {
let output = std ::process ::Command ::new ( " git " )
render_diff_report_for ( & env ::current_dir ( ) ? )
. args ( [ " diff " , " -- " , " :(exclude).omx " ] )
. current_dir ( env ::current_dir ( ) ? )
. output ( ) ? ;
if ! output . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & output . stderr ) . trim ( ) . to_string ( ) ;
return Err ( format! ( " git diff failed: {stderr} " ) . into ( ) ) ;
}
}
let diff = String ::from_utf8 ( output . stdout ) ? ;
if diff . trim ( ) . is_empty ( ) {
fn render_diff_report_for ( cwd : & Path ) -> Result < String , Box < dyn std ::error ::Error > > {
let staged = run_git_diff_command_in ( cwd , & [ " diff " , " --cached " ] ) ? ;
let unstaged = run_git_diff_command_in ( cwd , & [ " diff " ] ) ? ;
if staged . trim ( ) . is_empty ( ) & & unstaged . trim ( ) . is_empty ( ) {
return Ok (
return Ok (
" Diff \n Result clean working tree \n Detail no current changes "
" Diff \n Result clean working tree \n Detail no current changes "
. to_string ( ) ,
. to_string ( ) ,
) ;
) ;
}
}
Ok ( format! ( " Diff \n \n {} " , diff . trim_end ( ) ) )
let mut sections = Vec ::new ( ) ;
if ! staged . trim ( ) . is_empty ( ) {
sections . push ( format! ( " Staged changes: \n {} " , staged . trim_end ( ) ) ) ;
}
if ! unstaged . trim ( ) . is_empty ( ) {
sections . push ( format! ( " Unstaged changes: \n {} " , unstaged . trim_end ( ) ) ) ;
}
Ok ( format! ( " Diff \n \n {} " , sections . join ( " \n \n " ) ) )
}
fn run_git_diff_command_in (
cwd : & Path ,
args : & [ & str ] ,
) -> Result < String , Box < dyn std ::error ::Error > > {
let output = std ::process ::Command ::new ( " git " )
. args ( args )
. current_dir ( cwd )
. output ( ) ? ;
if ! output . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & output . stderr ) . trim ( ) . to_string ( ) ;
return Err ( format! ( " git {} failed: {stderr} " , args . join ( " " ) ) . into ( ) ) ;
}
Ok ( String ::from_utf8 ( output . stdout ) ? )
}
}
fn render_version_report ( ) -> String {
fn render_version_report ( ) -> String {
@@ -2489,9 +2411,9 @@ fn print_help() {
println! ( " rusty-claude-cli v {VERSION} " ) ;
println! ( " rusty-claude-cli v {VERSION} " ) ;
println! ( ) ;
println! ( ) ;
println! ( " Usage: " ) ;
println! ( " Usage: " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--max-cost USD] [--allowedTools TOOL[,TOOL...]] " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--allowedTools TOOL[,TOOL...]] " ) ;
println! ( " Start the interactive REPL " ) ;
println! ( " Start the interactive REPL " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--max-cost USD] [--output-format text|json] prompt TEXT " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--output-format text|json] prompt TEXT " ) ;
println! ( " Send one prompt and exit " ) ;
println! ( " Send one prompt and exit " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--output-format text|json] TEXT " ) ;
println! ( " rusty-claude-cli [--model MODEL] [--output-format text|json] TEXT " ) ;
println! ( " Shorthand non-interactive prompt mode " ) ;
println! ( " Shorthand non-interactive prompt mode " ) ;
@@ -2507,7 +2429,6 @@ fn print_help() {
println! ( " --model MODEL Override the active model " ) ;
println! ( " --model MODEL Override the active model " ) ;
println! ( " --output-format FORMAT Non-interactive output format: text or json " ) ;
println! ( " --output-format FORMAT Non-interactive output format: text or json " ) ;
println! ( " --permission-mode MODE Set read-only, workspace-write, or danger-full-access " ) ;
println! ( " --permission-mode MODE Set read-only, workspace-write, or danger-full-access " ) ;
println! ( " --max-cost USD Warn at 80% of budget and stop at/exceeding the budget " ) ;
println! ( " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported) " ) ;
println! ( " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported) " ) ;
println! ( " --version, -V Print version and build information locally " ) ;
println! ( " --version, -V Print version and build information locally " ) ;
println! ( ) ;
println! ( ) ;
@@ -2534,17 +2455,57 @@ fn print_help() {
#[ cfg(test) ]
#[ cfg(test) ]
mod tests {
mod tests {
use super ::{
use super ::{
budget_notice_message , filter_tool_specs, format_compact_report , format_cost_report ,
filter_tool_specs , format_compact_report , format_cost_report , format_init_report ,
format_init_report , format_ model_report , format_model_switch_report ,
format_model_report , format_model_switch_report , format_permissions_report ,
format_permissions_report , format_permissions_ switch_report , format_resume_report ,
format_permissions_switch_report , format_resume_report , format_status_report ,
format_status_repo rt , format_tool_call_star t , f ormat_tool_result ,
format_tool_call_sta rt , format_tool_resul t , n ormalize_permission_mode , parse_args ,
normalize_permission_mode , parse_args , parse_git_status_metadata , render_config_report ,
parse_git_status_branch , parse_git_status_metadata , render_config_report ,
render_init_claude_md , render_memory_report , render_repl_help ,
render_diff_report , render_ init_claude_md , render_memory_report , render_repl_help ,
resume_supported_slash_commands , status_context , CliAction , CliOutputFormat , SlashCommand ,
resume_supported_slash_commands , run_resume_command , status_context , CliAction ,
StatusUsage , DEFAULT_MODEL ,
CliOutputFormat , SlashCommand , StatusUsage, DEFAULT_MODEL ,
} ;
} ;
use runtime ::{ ContentBlock , ConversationMessage , MessageRole , PermissionMode } ;
use runtime ::{ ContentBlock , ConversationMessage , MessageRole , PermissionMode , Session };
use std ::fs ;
use std ::path ::{ Path , PathBuf } ;
use std ::path ::{ Path , PathBuf } ;
use std ::process ::Command ;
use std ::sync ::{ Mutex , MutexGuard , OnceLock } ;
use std ::time ::{ SystemTime , UNIX_EPOCH } ;
fn temp_dir ( ) -> PathBuf {
let nanos = SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. expect ( " time should be after epoch " )
. as_nanos ( ) ;
std ::env ::temp_dir ( ) . join ( format! ( " rusty-claude-cli- {nanos} " ) )
}
fn git ( args : & [ & str ] , cwd : & Path ) {
let status = Command ::new ( " git " )
. args ( args )
. current_dir ( cwd )
. status ( )
. expect ( " git command should run " ) ;
assert! (
status . success ( ) ,
" git command failed: git {} " ,
args . join ( " " )
) ;
}
fn env_lock ( ) -> MutexGuard < 'static , ( ) > {
static LOCK : OnceLock < Mutex < ( ) > > = OnceLock ::new ( ) ;
LOCK . get_or_init ( | | Mutex ::new ( ( ) ) )
. lock ( )
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
}
fn with_current_dir < T > ( cwd : & Path , f : impl FnOnce ( ) -> T ) -> T {
let previous = std ::env ::current_dir ( ) . expect ( " cwd should load " ) ;
std ::env ::set_current_dir ( cwd ) . expect ( " cwd should change " ) ;
let result = f ( ) ;
std ::env ::set_current_dir ( previous ) . expect ( " cwd should restore " ) ;
result
}
#[ test ]
#[ test ]
fn defaults_to_repl_when_no_args ( ) {
fn defaults_to_repl_when_no_args ( ) {
@@ -2554,7 +2515,6 @@ mod tests {
model : DEFAULT_MODEL . to_string ( ) ,
model : DEFAULT_MODEL . to_string ( ) ,
allowed_tools : None ,
allowed_tools : None ,
permission_mode : PermissionMode ::WorkspaceWrite ,
permission_mode : PermissionMode ::WorkspaceWrite ,
max_cost_usd : None ,
}
}
) ;
) ;
}
}
@@ -2574,7 +2534,6 @@ mod tests {
output_format : CliOutputFormat ::Text ,
output_format : CliOutputFormat ::Text ,
allowed_tools : None ,
allowed_tools : None ,
permission_mode : PermissionMode ::WorkspaceWrite ,
permission_mode : PermissionMode ::WorkspaceWrite ,
max_cost_usd : None ,
}
}
) ;
) ;
}
}
@@ -2596,7 +2555,6 @@ mod tests {
output_format : CliOutputFormat ::Json ,
output_format : CliOutputFormat ::Json ,
allowed_tools : None ,
allowed_tools : None ,
permission_mode : PermissionMode ::WorkspaceWrite ,
permission_mode : PermissionMode ::WorkspaceWrite ,
max_cost_usd : None ,
}
}
) ;
) ;
}
}
@@ -2622,32 +2580,10 @@ mod tests {
model : DEFAULT_MODEL . to_string ( ) ,
model : DEFAULT_MODEL . to_string ( ) ,
allowed_tools : None ,
allowed_tools : None ,
permission_mode : PermissionMode ::ReadOnly ,
permission_mode : PermissionMode ::ReadOnly ,
max_cost_usd : None ,
}
}
) ;
) ;
}
}
#[ test ]
fn parses_max_cost_flag ( ) {
let args = vec! [ " --max-cost=1.25 " . to_string ( ) ] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Repl {
model : DEFAULT_MODEL . to_string ( ) ,
allowed_tools : None ,
permission_mode : PermissionMode ::WorkspaceWrite ,
max_cost_usd : Some ( 1.25 ) ,
}
) ;
}
#[ test ]
fn rejects_invalid_max_cost_flag ( ) {
let error = parse_args ( & [ " --max-cost " . to_string ( ) , " 0 " . to_string ( ) ] )
. expect_err ( " zero max cost should be rejected " ) ;
assert! ( error . contains ( " --max-cost must be a positive finite USD amount " ) ) ;
}
#[ test ]
#[ test ]
fn parses_allowed_tools_flags_with_aliases_and_lists ( ) {
fn parses_allowed_tools_flags_with_aliases_and_lists ( ) {
let args = vec! [
let args = vec! [
@@ -2666,7 +2602,6 @@ mod tests {
. collect ( )
. collect ( )
) ,
) ,
permission_mode : PermissionMode ::WorkspaceWrite ,
permission_mode : PermissionMode ::WorkspaceWrite ,
max_cost_usd : None ,
}
}
) ;
) ;
}
}
@@ -2824,24 +2759,18 @@ mod tests {
#[ test ]
#[ test ]
fn cost_report_uses_sectioned_layout ( ) {
fn cost_report_uses_sectioned_layout ( ) {
let report = format_cost_report (
let report = format_cost_report ( runtime ::TokenUsage {
" claude-sonnet " ,
runtime ::TokenUsage {
input_tokens : 20 ,
input_tokens : 20 ,
output_tokens : 8 ,
output_tokens : 8 ,
cache_creation_input_tokens : 3 ,
cache_creation_input_tokens : 3 ,
cache_read_input_tokens : 1 ,
cache_read_input_tokens : 1 ,
} ,
} ) ;
None ,
) ;
assert! ( report . contains ( " Cost " ) ) ;
assert! ( report . contains ( " Cost " ) ) ;
assert! ( report . contains ( " Input tokens 20 " ) ) ;
assert! ( report . contains ( " Input tokens 20 " ) ) ;
assert! ( report . contains ( " Output tokens 8 " ) ) ;
assert! ( report . contains ( " Output tokens 8 " ) ) ;
assert! ( report . contains ( " Cache create 3 " ) ) ;
assert! ( report . contains ( " Cache create 3 " ) ) ;
assert! ( report . contains ( " Cache read 1 " ) ) ;
assert! ( report . contains ( " Cache read 1 " ) ) ;
assert! ( report . contains ( " Total tokens 32 " ) ) ;
assert! ( report . contains ( " Total tokens 32 " ) ) ;
assert! ( report . contains ( " Estimated cost " ) ) ;
assert! ( report . contains ( " Budget $0.0010 (unlimited) " ) ) ;
}
}
#[ test ]
#[ test ]
@@ -2923,7 +2852,6 @@ mod tests {
project_root : Some ( PathBuf ::from ( " /tmp " ) ) ,
project_root : Some ( PathBuf ::from ( " /tmp " ) ) ,
git_branch : Some ( " main " . to_string ( ) ) ,
git_branch : Some ( " main " . to_string ( ) ) ,
} ,
} ,
Some ( 1.0 ) ,
) ;
) ;
assert! ( status . contains ( " Status " ) ) ;
assert! ( status . contains ( " Status " ) ) ;
assert! ( status . contains ( " Model claude-sonnet " ) ) ;
assert! ( status . contains ( " Model claude-sonnet " ) ) ;
@@ -2931,7 +2859,6 @@ mod tests {
assert! ( status . contains ( " Messages 7 " ) ) ;
assert! ( status . contains ( " Messages 7 " ) ) ;
assert! ( status . contains ( " Latest total 10 " ) ) ;
assert! ( status . contains ( " Latest total 10 " ) ) ;
assert! ( status . contains ( " Cumulative total 31 " ) ) ;
assert! ( status . contains ( " Cumulative total 31 " ) ) ;
assert! ( status . contains ( " Cost budget $0.0009 / $1.0000 " ) ) ;
assert! ( status . contains ( " Cwd /tmp/project " ) ) ;
assert! ( status . contains ( " Cwd /tmp/project " ) ) ;
assert! ( status . contains ( " Project root /tmp " ) ) ;
assert! ( status . contains ( " Project root /tmp " ) ) ;
assert! ( status . contains ( " Git branch main " ) ) ;
assert! ( status . contains ( " Git branch main " ) ) ;
@@ -2940,22 +2867,6 @@ mod tests {
assert! ( status . contains ( " Memory files 4 " ) ) ;
assert! ( status . contains ( " Memory files 4 " ) ) ;
}
}
#[ test ]
fn budget_notice_warns_near_limit ( ) {
let message = budget_notice_message (
" claude-sonnet " ,
runtime ::TokenUsage {
input_tokens : 60_000 ,
output_tokens : 0 ,
cache_creation_input_tokens : 0 ,
cache_read_input_tokens : 0 ,
} ,
Some ( 1.0 ) ,
)
. expect ( " budget warning expected " ) ;
assert! ( message . contains ( " approaching cost budget " ) ) ;
}
#[ test ]
#[ test ]
fn config_report_supports_section_views ( ) {
fn config_report_supports_section_views ( ) {
let report = render_config_report ( Some ( " env " ) ) . expect ( " config report should render " ) ;
let report = render_config_report ( Some ( " env " ) ) . expect ( " config report should render " ) ;
@@ -2981,12 +2892,133 @@ mod tests {
#[ test ]
#[ test ]
fn parses_git_status_metadata ( ) {
fn parses_git_status_metadata ( ) {
let ( root , branch ) = parse_git_status_metadata ( Some (
let _guard = env_lock ( ) ;
let temp_root = temp_dir ( ) ;
fs ::create_dir_all ( & temp_root ) . expect ( " root dir " ) ;
let ( project_root , branch ) = with_current_dir ( & temp_root , | | {
parse_git_status_metadata ( Some (
" ## rcc/cli...origin/rcc/cli
" ## rcc/cli...origin/rcc/cli
M src/main.rs " ,
M src/main.rs " ,
) ) ;
) )
} ) ;
assert_eq! ( branch . as_deref ( ) , Some ( " rcc/cli " ) ) ;
assert_eq! ( branch . as_deref ( ) , Some ( " rcc/cli " ) ) ;
let _ = root ;
assert! ( project_root . is_none ( ) ) ;
fs ::remove_dir_all ( temp_root ) . expect ( " cleanup temp dir " ) ;
}
#[ test ]
fn parses_detached_head_from_status_snapshot ( ) {
let _guard = env_lock ( ) ;
assert_eq! (
parse_git_status_branch ( Some (
" ## HEAD (no branch)
M src/main.rs "
) ) ,
Some ( " detached HEAD " . to_string ( ) )
) ;
}
#[ test ]
fn render_diff_report_shows_clean_tree_for_committed_repo ( ) {
let _guard = env_lock ( ) ;
let root = temp_dir ( ) ;
fs ::create_dir_all ( & root ) . expect ( " root dir " ) ;
git ( & [ " init " , " --quiet " ] , & root ) ;
git ( & [ " config " , " user.email " , " tests@example.com " ] , & root ) ;
git ( & [ " config " , " user.name " , " Rusty Claude Tests " ] , & root ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n " ) . expect ( " write file " ) ;
git ( & [ " add " , " tracked.txt " ] , & root ) ;
git ( & [ " commit " , " -m " , " init " , " --quiet " ] , & root ) ;
let report = with_current_dir ( & root , | | {
render_diff_report ( ) . expect ( " diff report should render " )
} ) ;
assert! ( report . contains ( " clean working tree " ) ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
}
#[ test ]
fn render_diff_report_includes_staged_and_unstaged_sections ( ) {
let _guard = env_lock ( ) ;
let root = temp_dir ( ) ;
fs ::create_dir_all ( & root ) . expect ( " root dir " ) ;
git ( & [ " init " , " --quiet " ] , & root ) ;
git ( & [ " config " , " user.email " , " tests@example.com " ] , & root ) ;
git ( & [ " config " , " user.name " , " Rusty Claude Tests " ] , & root ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n " ) . expect ( " write file " ) ;
git ( & [ " add " , " tracked.txt " ] , & root ) ;
git ( & [ " commit " , " -m " , " init " , " --quiet " ] , & root ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n staged \n " ) . expect ( " update file " ) ;
git ( & [ " add " , " tracked.txt " ] , & root ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n staged \n unstaged \n " )
. expect ( " update file twice " ) ;
let report = with_current_dir ( & root , | | {
render_diff_report ( ) . expect ( " diff report should render " )
} ) ;
assert! ( report . contains ( " Staged changes: " ) ) ;
assert! ( report . contains ( " Unstaged changes: " ) ) ;
assert! ( report . contains ( " tracked.txt " ) ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
}
#[ test ]
fn render_diff_report_omits_ignored_files ( ) {
let _guard = env_lock ( ) ;
let root = temp_dir ( ) ;
fs ::create_dir_all ( & root ) . expect ( " root dir " ) ;
git ( & [ " init " , " --quiet " ] , & root ) ;
git ( & [ " config " , " user.email " , " tests@example.com " ] , & root ) ;
git ( & [ " config " , " user.name " , " Rusty Claude Tests " ] , & root ) ;
fs ::write ( root . join ( " .gitignore " ) , " .omx/ \n ignored.txt \n " ) . expect ( " write gitignore " ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n " ) . expect ( " write tracked " ) ;
git ( & [ " add " , " .gitignore " , " tracked.txt " ] , & root ) ;
git ( & [ " commit " , " -m " , " init " , " --quiet " ] , & root ) ;
fs ::create_dir_all ( root . join ( " .omx " ) ) . expect ( " write omx dir " ) ;
fs ::write ( root . join ( " .omx " ) . join ( " state.json " ) , " {} " ) . expect ( " write ignored omx " ) ;
fs ::write ( root . join ( " ignored.txt " ) , " secret \n " ) . expect ( " write ignored file " ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n world \n " ) . expect ( " write tracked change " ) ;
let report = with_current_dir ( & root , | | {
render_diff_report ( ) . expect ( " diff report should render " )
} ) ;
assert! ( report . contains ( " tracked.txt " ) ) ;
assert! ( ! report . contains ( " +++ b/ignored.txt " ) ) ;
assert! ( ! report . contains ( " +++ b/.omx/state.json " ) ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
}
#[ test ]
fn resume_diff_command_renders_report_for_saved_session ( ) {
let _guard = env_lock ( ) ;
let root = temp_dir ( ) ;
fs ::create_dir_all ( & root ) . expect ( " root dir " ) ;
git ( & [ " init " , " --quiet " ] , & root ) ;
git ( & [ " config " , " user.email " , " tests@example.com " ] , & root ) ;
git ( & [ " config " , " user.name " , " Rusty Claude Tests " ] , & root ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n " ) . expect ( " write tracked " ) ;
git ( & [ " add " , " tracked.txt " ] , & root ) ;
git ( & [ " commit " , " -m " , " init " , " --quiet " ] , & root ) ;
fs ::write ( root . join ( " tracked.txt " ) , " hello \n world \n " ) . expect ( " modify tracked " ) ;
let session_path = root . join ( " session.json " ) ;
Session ::new ( )
. save_to_path ( & session_path )
. expect ( " session should save " ) ;
let session = Session ::load_from_path ( & session_path ) . expect ( " session should load " ) ;
let outcome = with_current_dir ( & root , | | {
run_resume_command ( & session_path , & session , & SlashCommand ::Diff )
. expect ( " resume diff should work " )
} ) ;
let message = outcome . message . expect ( " diff message should exist " ) ;
assert! ( message . contains ( " Unstaged changes: " ) ) ;
assert! ( message . contains ( " tracked.txt " ) ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
}
}
#[ test ]
#[ test ]
@@ -2994,7 +3026,7 @@ mod tests {
let context = status_context ( None ) . expect ( " status context should load " ) ;
let context = status_context ( None ) . expect ( " status context should load " ) ;
assert! ( context . cwd . is_absolute ( ) ) ;
assert! ( context . cwd . is_absolute ( ) ) ;
assert! ( context . discovered_config_files > = context . loaded_config_files ) ;
assert! ( context . discovered_config_files > = context . loaded_config_files ) ;
assert! ( context . discover ed_config_files > = 1 ) ;
assert! ( context . load ed_config_files < = context . discovered_config_files ) ;
}
}
#[ test ]
#[ test ]