2026-04-02 10:04:54 +00:00
#![ allow(
dead_code ,
unused_imports ,
unused_variables ,
clippy ::unneeded_struct_pattern ,
clippy ::unnecessary_wraps ,
clippy ::unused_self
) ]
2026-04-01 01:14:44 +00:00
mod init ;
2026-03-31 18:39:39 +00:00
mod input ;
mod render ;
2026-04-01 04:30:28 +00:00
use std ::collections ::BTreeSet ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
use std ::env ;
2026-03-31 19:57:38 +00:00
use std ::fs ;
2026-03-31 23:38:05 +00:00
use std ::io ::{ self , Read , Write } ;
use std ::net ::TcpListener ;
2026-04-02 10:04:54 +00:00
use std ::ops ::{ Deref , DerefMut } ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
use std ::path ::{ Path , PathBuf } ;
2026-03-31 23:38:05 +00:00
use std ::process ::Command ;
2026-04-02 11:05:03 +09:00
use std ::sync ::mpsc ::{ self , Receiver , RecvTimeoutError , Sender } ;
2026-04-01 08:03:22 +00:00
use std ::sync ::{ Arc , Mutex } ;
2026-04-01 05:55:24 +00:00
use std ::thread ::{ self , JoinHandle } ;
2026-04-02 11:10:48 +09:00
use std ::time ::{ Duration , Instant , UNIX_EPOCH } ;
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 18:39:39 +00:00
use api ::{
2026-04-01 03:55:00 +00:00
resolve_startup_auth_source , AnthropicClient , AuthSource , ContentBlockDelta , InputContentBlock ,
2026-04-02 11:38:24 +09:00
InputMessage , MessageRequest , MessageResponse , OutputContentBlock , PromptCache ,
2026-04-02 14:00:07 +09:00
StreamEvent as ApiStreamEvent , ToolChoice , ToolDefinition , ToolResultContentBlock ,
2026-03-31 18:39:39 +00:00
} ;
2026-04-01 00:14:38 +00:00
use commands ::{
2026-04-02 10:04:40 +00:00
handle_agents_slash_command , handle_mcp_slash_command , handle_plugins_slash_command ,
handle_skills_slash_command , render_slash_command_help , resume_supported_slash_commands ,
slash_command_specs , validate_slash_command_input , SlashCommand ,
2026-04-01 00:14:38 +00:00
} ;
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
use compat_harness ::{ extract_manifest , UpstreamPaths } ;
2026-04-01 01:14:44 +00:00
use init ::initialize_repo ;
2026-04-02 10:04:54 +00:00
use plugins ::{ PluginHooks , PluginManager , PluginManagerConfig , PluginRegistry } ;
2026-04-01 03:14:45 +00:00
use render ::{ MarkdownStreamState , Spinner , TerminalRenderer } ;
2026-03-31 18:39:39 +00:00
use runtime ::{
2026-04-04 00:42:43 +09:00
clear_oauth_credentials , format_usd , generate_pkce_pair , generate_state , load_system_prompt ,
parse_oauth_callback_request_target , pricing_for_model , resolve_sandbox_status ,
save_oauth_credentials , ApiClient , ApiRequest , AssistantEvent , CompactionConfig , ConfigLoader ,
ConfigSource , ContentBlock , ConversationMessage , ConversationRuntime , McpServerManager ,
McpTool , MessageRole , ModelPricing , OAuthAuthorizationRequest , OAuthConfig ,
OAuthTokenExchangeRequest , PermissionMode , PermissionPolicy , ProjectContext , PromptCacheEvent ,
ResolvedPermissionMode , RuntimeError , Session , TokenUsage , ToolError , ToolExecutor ,
UsageTracker ,
2026-03-31 18:39:39 +00:00
} ;
2026-04-03 14:31:25 +00:00
use serde ::Deserialize ;
2026-03-31 22:49:50 +00:00
use serde_json ::json ;
2026-04-03 14:31:25 +00:00
use tools ::{ GlobalToolRegistry , RuntimeToolDefinition , ToolSearchOutput } ;
2026-03-31 18:39:39 +00:00
2026-04-01 01:48:21 +00:00
const DEFAULT_MODEL : & str = " claude-opus-4-6 " ;
2026-04-01 03:55:00 +00:00
fn max_tokens_for_model ( model : & str ) -> u32 {
if model . contains ( " opus " ) {
32_000
} else {
64_000
}
}
2026-03-31 18:39:39 +00:00
const DEFAULT_DATE : & str = " 2026-03-31 " ;
2026-03-31 23:38:05 +00:00
const DEFAULT_OAUTH_CALLBACK_PORT : u16 = 4545 ;
2026-03-31 22:49:50 +00:00
const VERSION : & str = env! ( " CARGO_PKG_VERSION " ) ;
const BUILD_TARGET : Option < & str > = option_env! ( " TARGET " ) ;
const GIT_SHA : Option < & str > = option_env! ( " GIT_SHA " ) ;
2026-04-01 08:03:22 +00:00
const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL : Duration = Duration ::from_secs ( 3 ) ;
2026-04-01 06:15:14 +00:00
const PRIMARY_SESSION_EXTENSION : & str = " jsonl " ;
const LEGACY_SESSION_EXTENSION : & str = " json " ;
2026-04-02 07:15:03 +00:00
const LATEST_SESSION_REFERENCE : & str = " latest " ;
const SESSION_REFERENCE_ALIASES : & [ & str ] = & [ LATEST_SESSION_REFERENCE , " last " , " recent " ] ;
const CLI_OPTION_SUGGESTIONS : & [ & str ] = & [
" --help " ,
" -h " ,
" --version " ,
" -V " ,
" --model " ,
" --output-format " ,
" --permission-mode " ,
" --dangerously-skip-permissions " ,
" --allowedTools " ,
" --allowed-tools " ,
" --resume " ,
" --print " ,
" -p " ,
] ;
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 23:38:53 +00:00
type AllowedToolSet = BTreeSet < String > ;
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
fn main ( ) {
2026-03-31 18:39:39 +00:00
if let Err ( error ) = run ( ) {
2026-04-02 07:15:03 +00:00
let message = error . to_string ( ) ;
if message . contains ( " `claw --help` " ) {
eprintln! ( " error: {message} " ) ;
} else {
eprintln! (
" error: {message}
2026-03-31 22:49:50 +00:00
2026-04-01 01:44:55 +00:00
Run ` claw - - help ` for usage . "
2026-04-02 07:15:03 +00:00
) ;
}
2026-03-31 18:39:39 +00:00
std ::process ::exit ( 1 ) ;
}
}
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 18:39:39 +00:00
fn run ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let args : Vec < String > = env ::args ( ) . skip ( 1 ) . collect ( ) ;
match parse_args ( & args ) ? {
CliAction ::DumpManifests = > dump_manifests ( ) ,
CliAction ::BootstrapPlan = > print_bootstrap_plan ( ) ,
2026-04-01 08:30:02 +00:00
CliAction ::Agents { args } = > LiveCli ::print_agents ( args . as_deref ( ) ) ? ,
2026-04-02 10:04:40 +00:00
CliAction ::Mcp { args } = > LiveCli ::print_mcp ( args . as_deref ( ) ) ? ,
2026-04-01 08:30:02 +00:00
CliAction ::Skills { args } = > LiveCli ::print_skills ( args . as_deref ( ) ) ? ,
2026-03-31 18:39:39 +00:00
CliAction ::PrintSystemPrompt { cwd , date } = > print_system_prompt ( cwd , date ) ,
2026-03-31 23:38:53 +00:00
CliAction ::Version = > print_version ( ) ,
2026-03-31 18:39:39 +00:00
CliAction ::ResumeSession {
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
session_path ,
2026-03-31 20:00:13 +00:00
commands ,
} = > resume_session ( & session_path , & commands ) ,
2026-04-02 07:44:39 +00:00
CliAction ::Status {
model ,
permission_mode ,
} = > print_status_snapshot ( & model , permission_mode ) ? ,
CliAction ::Sandbox = > print_sandbox_status_snapshot ( ) ? ,
2026-03-31 22:49:50 +00:00
CliAction ::Prompt {
prompt ,
model ,
output_format ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-04-01 00:06:15 +00:00
permission_mode ,
2026-04-01 02:42:49 +00:00
} = > LiveCli ::new ( model , true , allowed_tools , permission_mode ) ?
2026-03-31 23:38:53 +00:00
. run_turn_with_output ( & prompt , output_format ) ? ,
2026-03-31 23:38:05 +00:00
CliAction ::Login = > run_login ( ) ? ,
CliAction ::Logout = > run_logout ( ) ? ,
2026-04-01 01:14:44 +00:00
CliAction ::Init = > run_init ( ) ? ,
2026-03-31 23:38:53 +00:00
CliAction ::Repl {
model ,
allowed_tools ,
2026-04-01 00:06:15 +00:00
permission_mode ,
} = > run_repl ( model , allowed_tools , permission_mode ) ? ,
2026-03-31 18:39:39 +00:00
CliAction ::Help = > print_help ( ) ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-03-31 18:39:39 +00:00
Ok ( ( ) )
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
#[ derive(Debug, Clone, PartialEq, Eq) ]
enum CliAction {
DumpManifests ,
BootstrapPlan ,
2026-04-01 08:30:02 +00:00
Agents {
args : Option < String > ,
} ,
2026-04-02 10:04:40 +00:00
Mcp {
args : Option < String > ,
} ,
2026-04-01 08:30:02 +00:00
Skills {
args : Option < String > ,
} ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
PrintSystemPrompt {
cwd : PathBuf ,
date : String ,
} ,
2026-03-31 23:38:53 +00:00
Version ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
ResumeSession {
session_path : PathBuf ,
2026-03-31 20:00:13 +00:00
commands : Vec < String > ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
} ,
2026-04-02 07:44:39 +00:00
Status {
model : String ,
permission_mode : PermissionMode ,
} ,
Sandbox ,
2026-03-31 18:39:39 +00:00
Prompt {
prompt : String ,
model : String ,
2026-03-31 22:49:50 +00:00
output_format : CliOutputFormat ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 00:06:15 +00:00
permission_mode : PermissionMode ,
2026-03-31 18:39:39 +00:00
} ,
2026-03-31 23:38:05 +00:00
Login ,
Logout ,
2026-04-01 01:14:44 +00:00
Init ,
2026-03-31 18:39:39 +00:00
Repl {
model : String ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 00:06:15 +00:00
permission_mode : PermissionMode ,
2026-03-31 18:39:39 +00:00
} ,
2026-03-31 22:49:50 +00:00
// prompt-mode formatting is only supported for non-interactive runs
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
Help ,
}
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
2026-03-31 22:49:50 +00:00
#[ derive(Debug, Clone, Copy, PartialEq, Eq) ]
enum CliOutputFormat {
Text ,
Json ,
}
impl CliOutputFormat {
fn parse ( value : & str ) -> Result < Self , String > {
match value {
" text " = > Ok ( Self ::Text ) ,
" json " = > Ok ( Self ::Json ) ,
other = > Err ( format! (
" unsupported value for --output-format: {other} (expected text or json) "
) ) ,
}
}
}
2026-04-01 00:06:15 +00:00
#[ allow(clippy::too_many_lines) ]
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn parse_args ( args : & [ String ] ) -> Result < CliAction , String > {
2026-03-31 18:39:39 +00:00
let mut model = DEFAULT_MODEL . to_string ( ) ;
2026-03-31 22:49:50 +00:00
let mut output_format = CliOutputFormat ::Text ;
2026-04-01 00:06:15 +00:00
let mut permission_mode = default_permission_mode ( ) ;
2026-04-02 07:15:03 +00:00
let mut wants_help = false ;
2026-03-31 23:38:53 +00:00
let mut wants_version = false ;
let mut allowed_tool_values = Vec ::new ( ) ;
2026-03-31 18:39:39 +00:00
let mut rest = Vec ::new ( ) ;
let mut index = 0 ;
while index < args . len ( ) {
match args [ index ] . as_str ( ) {
2026-04-02 07:15:03 +00:00
" --help " | " -h " if rest . is_empty ( ) = > {
wants_help = true ;
index + = 1 ;
}
2026-03-31 23:38:53 +00:00
" --version " | " -V " = > {
wants_version = true ;
index + = 1 ;
}
2026-03-31 18:39:39 +00:00
" --model " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --model " . to_string ( ) ) ? ;
2026-04-01 02:04:12 +00:00
model = resolve_model_alias ( value ) . to_string ( ) ;
2026-03-31 18:39:39 +00:00
index + = 2 ;
}
flag if flag . starts_with ( " --model= " ) = > {
2026-04-01 02:04:12 +00:00
model = resolve_model_alias ( & flag [ 8 .. ] ) . to_string ( ) ;
2026-03-31 18:39:39 +00:00
index + = 1 ;
}
2026-03-31 22:49:50 +00:00
" --output-format " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --output-format " . to_string ( ) ) ? ;
output_format = CliOutputFormat ::parse ( value ) ? ;
index + = 2 ;
}
2026-04-01 00:06:15 +00:00
" --permission-mode " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --permission-mode " . to_string ( ) ) ? ;
permission_mode = parse_permission_mode_arg ( value ) ? ;
index + = 2 ;
}
2026-03-31 22:49:50 +00:00
flag if flag . starts_with ( " --output-format= " ) = > {
output_format = CliOutputFormat ::parse ( & flag [ 16 .. ] ) ? ;
index + = 1 ;
}
2026-04-01 00:06:15 +00:00
flag if flag . starts_with ( " --permission-mode= " ) = > {
permission_mode = parse_permission_mode_arg ( & flag [ 18 .. ] ) ? ;
index + = 1 ;
}
2026-04-01 02:18:23 +00:00
" --dangerously-skip-permissions " = > {
permission_mode = PermissionMode ::DangerFullAccess ;
index + = 1 ;
}
2026-04-01 03:22:34 +00:00
" -p " = > {
2026-04-01 18:57:50 +09:00
// Claw Code compat: -p "prompt" = one-shot prompt
2026-04-01 03:22:34 +00:00
let prompt = args [ index + 1 .. ] . join ( " " ) ;
if prompt . trim ( ) . is_empty ( ) {
return Err ( " -p requires a prompt string " . to_string ( ) ) ;
}
return Ok ( CliAction ::Prompt {
prompt ,
model : resolve_model_alias ( & model ) . to_string ( ) ,
output_format ,
allowed_tools : normalize_allowed_tools ( & allowed_tool_values ) ? ,
permission_mode ,
} ) ;
}
" --print " = > {
2026-04-01 18:57:50 +09:00
// Claw Code compat: --print makes output non-interactive
2026-04-01 03:22:34 +00:00
output_format = CliOutputFormat ::Text ;
index + = 1 ;
}
2026-04-02 07:15:03 +00:00
" --resume " if rest . is_empty ( ) = > {
rest . push ( " --resume " . to_string ( ) ) ;
index + = 1 ;
}
flag if rest . is_empty ( ) & & flag . starts_with ( " --resume= " ) = > {
rest . push ( " --resume " . to_string ( ) ) ;
rest . push ( flag [ 9 .. ] . to_string ( ) ) ;
index + = 1 ;
}
2026-03-31 23:38:53 +00:00
" --allowedTools " | " --allowed-tools " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --allowedTools " . to_string ( ) ) ? ;
allowed_tool_values . push ( value . clone ( ) ) ;
index + = 2 ;
}
flag if flag . starts_with ( " --allowedTools= " ) = > {
allowed_tool_values . push ( flag [ 15 .. ] . to_string ( ) ) ;
index + = 1 ;
}
flag if flag . starts_with ( " --allowed-tools= " ) = > {
allowed_tool_values . push ( flag [ 16 .. ] . to_string ( ) ) ;
index + = 1 ;
}
2026-04-02 07:15:03 +00:00
other if rest . is_empty ( ) & & other . starts_with ( '-' ) = > {
return Err ( format_unknown_option ( other ) )
}
2026-03-31 18:39:39 +00:00
other = > {
rest . push ( other . to_string ( ) ) ;
index + = 1 ;
}
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-04-02 07:15:03 +00:00
if wants_help {
return Ok ( CliAction ::Help ) ;
}
2026-03-31 23:38:53 +00:00
if wants_version {
return Ok ( CliAction ::Version ) ;
}
let allowed_tools = normalize_allowed_tools ( & allowed_tool_values ) ? ;
2026-03-31 18:39:39 +00:00
if rest . is_empty ( ) {
2026-03-31 23:38:53 +00:00
return Ok ( CliAction ::Repl {
model ,
allowed_tools ,
2026-04-01 00:06:15 +00:00
permission_mode ,
2026-03-31 23:38:53 +00:00
} ) ;
2026-03-31 18:39:39 +00:00
}
if rest . first ( ) . map ( String ::as_str ) = = Some ( " --resume " ) {
return parse_resume_args ( & rest [ 1 .. ] ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-04-02 07:44:39 +00:00
if let Some ( action ) = parse_single_word_command_alias ( & rest , & model , permission_mode ) {
return action ;
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
2026-03-31 18:39:39 +00:00
match rest [ 0 ] . as_str ( ) {
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
" dump-manifests " = > Ok ( CliAction ::DumpManifests ) ,
" bootstrap-plan " = > Ok ( CliAction ::BootstrapPlan ) ,
2026-04-01 08:30:02 +00:00
" agents " = > Ok ( CliAction ::Agents {
args : join_optional_args ( & rest [ 1 .. ] ) ,
} ) ,
2026-04-02 10:04:40 +00:00
" mcp " = > Ok ( CliAction ::Mcp {
args : join_optional_args ( & rest [ 1 .. ] ) ,
} ) ,
2026-04-01 08:30:02 +00:00
" skills " = > Ok ( CliAction ::Skills {
args : join_optional_args ( & rest [ 1 .. ] ) ,
} ) ,
2026-03-31 18:39:39 +00:00
" system-prompt " = > parse_system_prompt_args ( & rest [ 1 .. ] ) ,
2026-03-31 23:38:05 +00:00
" login " = > Ok ( CliAction ::Login ) ,
" logout " = > Ok ( CliAction ::Logout ) ,
2026-04-01 01:14:44 +00:00
" init " = > Ok ( CliAction ::Init ) ,
2026-03-31 18:39:39 +00:00
" prompt " = > {
let prompt = rest [ 1 .. ] . join ( " " ) ;
if prompt . trim ( ) . is_empty ( ) {
return Err ( " prompt subcommand requires a prompt string " . to_string ( ) ) ;
}
2026-03-31 22:49:50 +00:00
Ok ( CliAction ::Prompt {
prompt ,
model ,
output_format ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-04-01 00:06:15 +00:00
permission_mode ,
2026-03-31 22:49:50 +00:00
} )
2026-03-31 18:39:39 +00:00
}
2026-04-01 08:30:02 +00:00
other if other . starts_with ( '/' ) = > parse_direct_slash_cli_action ( & rest ) ,
_other = > Ok ( CliAction ::Prompt {
2026-03-31 22:49:50 +00:00
prompt : rest . join ( " " ) ,
model ,
output_format ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-04-01 00:06:15 +00:00
permission_mode ,
2026-03-31 22:49:50 +00:00
} ) ,
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
}
2026-04-02 07:44:39 +00:00
fn parse_single_word_command_alias (
rest : & [ String ] ,
model : & str ,
permission_mode : PermissionMode ,
) -> Option < Result < CliAction , String > > {
if rest . len ( ) ! = 1 {
return None ;
}
match rest [ 0 ] . as_str ( ) {
" help " = > Some ( Ok ( CliAction ::Help ) ) ,
" version " = > Some ( Ok ( CliAction ::Version ) ) ,
" status " = > Some ( Ok ( CliAction ::Status {
model : model . to_string ( ) ,
permission_mode ,
} ) ) ,
" sandbox " = > Some ( Ok ( CliAction ::Sandbox ) ) ,
other = > bare_slash_command_guidance ( other ) . map ( Err ) ,
}
}
fn bare_slash_command_guidance ( command_name : & str ) -> Option < String > {
if matches! (
command_name ,
" dump-manifests "
| " bootstrap-plan "
| " agents "
2026-04-02 10:04:40 +00:00
| " mcp "
2026-04-02 07:44:39 +00:00
| " skills "
| " system-prompt "
| " login "
| " logout "
| " init "
| " prompt "
) {
return None ;
}
let slash_command = slash_command_specs ( )
2026-04-02 17:52:31 +09:00
. iter ( )
2026-04-02 07:44:39 +00:00
. find ( | spec | spec . name = = command_name ) ? ;
let guidance = if slash_command . resume_supported {
format! (
" `claw {command_name}` is a slash command. Use `claw --resume SESSION.jsonl /{command_name}` or start `claw` and run `/{command_name}`. "
)
} else {
format! (
" `claw {command_name}` is a slash command. Start `claw` and run `/{command_name}` inside the REPL. "
)
} ;
Some ( guidance )
}
2026-04-01 08:30:02 +00:00
fn join_optional_args ( args : & [ String ] ) -> Option < String > {
let joined = args . join ( " " ) ;
let trimmed = joined . trim ( ) ;
( ! trimmed . is_empty ( ) ) . then ( | | trimmed . to_string ( ) )
}
fn parse_direct_slash_cli_action ( rest : & [ String ] ) -> Result < CliAction , String > {
let raw = rest . join ( " " ) ;
match SlashCommand ::parse ( & raw ) {
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Help ) ) = > Ok ( CliAction ::Help ) ,
Ok ( Some ( SlashCommand ::Agents { args } ) ) = > Ok ( CliAction ::Agents { args } ) ,
2026-04-02 10:04:40 +00:00
Ok ( Some ( SlashCommand ::Mcp { action , target } ) ) = > Ok ( CliAction ::Mcp {
args : match ( action , target ) {
( None , None ) = > None ,
( Some ( action ) , None ) = > Some ( action ) ,
( Some ( action ) , Some ( target ) ) = > Some ( format! ( " {action} {target} " ) ) ,
( None , Some ( target ) ) = > Some ( target ) ,
} ,
} ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Skills { args } ) ) = > Ok ( CliAction ::Skills { args } ) ,
Ok ( Some ( SlashCommand ::Unknown ( name ) ) ) = > Err ( format_unknown_direct_slash_command ( & name ) ) ,
Ok ( Some ( command ) ) = > Err ( {
2026-04-02 08:30:05 +00:00
let _ = command ;
format! (
" slash command {command_name} is interactive-only. Start `claw` and run it there, or use `claw --resume SESSION.jsonl {command_name}` / `claw --resume {latest} {command_name}` when the command is marked [resume] in /help. " ,
command_name = rest [ 0 ] ,
latest = LATEST_SESSION_REFERENCE ,
)
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
} ) ,
2026-04-02 18:09:48 +09:00
Ok ( None ) = > Err ( format! ( " unknown subcommand: {} " , rest [ 0 ] ) ) ,
Err ( error ) = > Err ( error . to_string ( ) ) ,
2026-03-31 23:38:53 +00:00
}
2026-04-01 03:55:00 +00:00
}
2026-03-31 23:38:53 +00:00
2026-04-02 07:15:03 +00:00
fn format_unknown_option ( option : & str ) -> String {
let mut message = format! ( " unknown option: {option} " ) ;
if let Some ( suggestion ) = suggest_closest_term ( option , CLI_OPTION_SUGGESTIONS ) {
message . push_str ( " \n Did you mean " ) ;
message . push_str ( suggestion ) ;
message . push ( '?' ) ;
}
message . push_str ( " \n Run `claw --help` for usage. " ) ;
message
}
fn format_unknown_direct_slash_command ( name : & str ) -> String {
let mut message = format! ( " unknown slash command outside the REPL: / {name} " ) ;
if let Some ( suggestions ) = render_suggestion_line ( " Did you mean " , & suggest_slash_commands ( name ) )
{
message . push ( '\n' ) ;
message . push_str ( & suggestions ) ;
}
message . push_str ( " \n Run `claw --help` for CLI usage, or start `claw` and use /help. " ) ;
message
}
fn format_unknown_slash_command ( name : & str ) -> String {
let mut message = format! ( " Unknown slash command: / {name} " ) ;
if let Some ( suggestions ) = render_suggestion_line ( " Did you mean " , & suggest_slash_commands ( name ) )
{
message . push ( '\n' ) ;
message . push_str ( & suggestions ) ;
}
message . push_str ( " \n Help /help lists available slash commands " ) ;
message
}
fn render_suggestion_line ( label : & str , suggestions : & [ String ] ) -> Option < String > {
( ! suggestions . is_empty ( ) ) . then ( | | format! ( " {label:<16} {} " , suggestions . join ( " , " ) , ) )
}
fn suggest_slash_commands ( input : & str ) -> Vec < String > {
let mut candidates = slash_command_specs ( )
. iter ( )
. flat_map ( | spec | {
std ::iter ::once ( spec . name )
. chain ( spec . aliases . iter ( ) . copied ( ) )
. map ( | name | format! ( " / {name} " ) )
. collect ::< Vec < _ > > ( )
} )
. collect ::< Vec < _ > > ( ) ;
candidates . sort ( ) ;
candidates . dedup ( ) ;
let candidate_refs = candidates . iter ( ) . map ( String ::as_str ) . collect ::< Vec < _ > > ( ) ;
ranked_suggestions ( input . trim_start_matches ( '/' ) , & candidate_refs )
. into_iter ( )
. map ( str ::to_string )
. collect ( )
}
fn suggest_closest_term < ' a > ( input : & str , candidates : & ' a [ & ' a str ] ) -> Option < & ' a str > {
ranked_suggestions ( input , candidates ) . into_iter ( ) . next ( )
}
fn ranked_suggestions < ' a > ( input : & str , candidates : & ' a [ & ' a str ] ) -> Vec < & ' a str > {
let normalized_input = input . trim_start_matches ( '/' ) . to_ascii_lowercase ( ) ;
let mut ranked = candidates
. iter ( )
. filter_map ( | candidate | {
let normalized_candidate = candidate . trim_start_matches ( '/' ) . to_ascii_lowercase ( ) ;
let distance = levenshtein_distance ( & normalized_input , & normalized_candidate ) ;
let prefix_bonus = usize ::from (
! ( normalized_candidate . starts_with ( & normalized_input )
| | normalized_input . starts_with ( & normalized_candidate ) ) ,
) ;
let score = distance + prefix_bonus ;
( score < = 4 ) . then_some ( ( score , * candidate ) )
} )
. collect ::< Vec < _ > > ( ) ;
ranked . sort_by ( | left , right | left . cmp ( right ) . then_with ( | | left . 1. cmp ( right . 1 ) ) ) ;
ranked
. into_iter ( )
. map ( | ( _ , candidate ) | candidate )
. take ( 3 )
. collect ( )
}
fn levenshtein_distance ( left : & str , right : & str ) -> usize {
if left . is_empty ( ) {
return right . chars ( ) . count ( ) ;
}
if right . is_empty ( ) {
return left . chars ( ) . count ( ) ;
}
let right_chars = right . chars ( ) . collect ::< Vec < _ > > ( ) ;
let mut previous = ( 0 ..= right_chars . len ( ) ) . collect ::< Vec < _ > > ( ) ;
let mut current = vec! [ 0 ; right_chars . len ( ) + 1 ] ;
for ( left_index , left_char ) in left . chars ( ) . enumerate ( ) {
current [ 0 ] = left_index + 1 ;
for ( right_index , right_char ) in right_chars . iter ( ) . enumerate ( ) {
let substitution_cost = usize ::from ( left_char ! = * right_char ) ;
current [ right_index + 1 ] = ( previous [ right_index + 1 ] + 1 )
. min ( current [ right_index ] + 1 )
. min ( previous [ right_index ] + substitution_cost ) ;
}
previous . clone_from ( & current ) ;
}
previous [ right_chars . len ( ) ]
}
2026-04-01 03:55:00 +00:00
fn resolve_model_alias ( model : & str ) -> & str {
match model {
" opus " = > " claude-opus-4-6 " ,
" sonnet " = > " claude-sonnet-4-6 " ,
" haiku " = > " claude-haiku-4-5-20251213 " ,
_ = > model ,
2026-03-31 23:38:53 +00:00
}
2026-04-01 03:55:00 +00:00
}
2026-03-31 23:38:53 +00:00
2026-04-01 03:55:00 +00:00
fn normalize_allowed_tools ( values : & [ String ] ) -> Result < Option < AllowedToolSet > , String > {
2026-04-01 07:22:41 +00:00
current_tool_registry ( ) ? . normalize_allowed_tools ( values )
2026-03-31 23:38:53 +00:00
}
2026-04-01 06:50:18 +00:00
fn current_tool_registry ( ) -> Result < GlobalToolRegistry , String > {
let cwd = env ::current_dir ( ) . map_err ( | error | error . to_string ( ) ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let runtime_config = loader . load ( ) . map_err ( | error | error . to_string ( ) ) ? ;
2026-04-03 14:31:25 +00:00
let state = build_runtime_plugin_state_with_loader ( & cwd , & loader , & runtime_config )
2026-04-01 06:50:18 +00:00
. map_err ( | error | error . to_string ( ) ) ? ;
2026-04-03 14:31:25 +00:00
let registry = state . tool_registry . clone ( ) ;
if let Some ( mcp_state ) = state . mcp_state {
mcp_state
. lock ( )
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
. shutdown ( )
. map_err ( | error | error . to_string ( ) ) ? ;
}
Ok ( registry )
2026-03-31 23:38:53 +00:00
}
2026-04-01 00:06:15 +00:00
fn parse_permission_mode_arg ( value : & str ) -> Result < PermissionMode , String > {
normalize_permission_mode ( value )
. ok_or_else ( | | {
format! (
" unsupported permission mode '{value}'. Use read-only, workspace-write, or danger-full-access. "
)
} )
. map ( permission_mode_from_label )
}
fn permission_mode_from_label ( mode : & str ) -> PermissionMode {
match mode {
" read-only " = > PermissionMode ::ReadOnly ,
" workspace-write " = > PermissionMode ::WorkspaceWrite ,
" danger-full-access " = > PermissionMode ::DangerFullAccess ,
other = > panic! ( " unsupported permission mode label: {other} " ) ,
}
}
2026-04-02 10:02:26 +00:00
fn permission_mode_from_resolved ( mode : ResolvedPermissionMode ) -> PermissionMode {
match mode {
ResolvedPermissionMode ::ReadOnly = > PermissionMode ::ReadOnly ,
ResolvedPermissionMode ::WorkspaceWrite = > PermissionMode ::WorkspaceWrite ,
ResolvedPermissionMode ::DangerFullAccess = > PermissionMode ::DangerFullAccess ,
}
}
2026-04-01 00:06:15 +00:00
fn default_permission_mode ( ) -> PermissionMode {
env ::var ( " RUSTY_CLAUDE_PERMISSION_MODE " )
. ok ( )
. as_deref ( )
. and_then ( normalize_permission_mode )
2026-04-02 10:02:26 +00:00
. map ( permission_mode_from_label )
. or_else ( config_permission_mode_for_current_dir )
. unwrap_or ( PermissionMode ::DangerFullAccess )
}
fn config_permission_mode_for_current_dir ( ) -> Option < PermissionMode > {
let cwd = env ::current_dir ( ) . ok ( ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
loader
. load ( )
. ok ( ) ?
. permission_mode ( )
. map ( permission_mode_from_resolved )
2026-04-01 00:06:15 +00:00
}
2026-04-01 06:50:18 +00:00
fn filter_tool_specs (
tool_registry : & GlobalToolRegistry ,
allowed_tools : Option < & AllowedToolSet > ,
) -> Vec < ToolDefinition > {
tool_registry . definitions ( allowed_tools )
2026-03-31 23:38:53 +00:00
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn parse_system_prompt_args ( args : & [ String ] ) -> Result < CliAction , String > {
let mut cwd = env ::current_dir ( ) . map_err ( | error | error . to_string ( ) ) ? ;
2026-03-31 18:39:39 +00:00
let mut date = DEFAULT_DATE . to_string ( ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
let mut index = 0 ;
while index < args . len ( ) {
match args [ index ] . as_str ( ) {
" --cwd " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --cwd " . to_string ( ) ) ? ;
cwd = PathBuf ::from ( value ) ;
index + = 2 ;
}
" --date " = > {
let value = args
. get ( index + 1 )
. ok_or_else ( | | " missing value for --date " . to_string ( ) ) ? ;
date . clone_from ( value ) ;
index + = 2 ;
}
other = > return Err ( format! ( " unknown system-prompt option: {other} " ) ) ,
}
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
Ok ( CliAction ::PrintSystemPrompt { cwd , date } )
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn parse_resume_args ( args : & [ String ] ) -> Result < CliAction , String > {
2026-04-02 08:35:56 +00:00
let ( session_path , command_tokens ) : ( PathBuf , & [ String ] ) = match args . first ( ) {
None = > ( PathBuf ::from ( LATEST_SESSION_REFERENCE ) , & [ ] ) ,
2026-04-02 08:40:34 +00:00
Some ( first ) if looks_like_slash_command_token ( first ) = > {
2026-04-02 08:35:56 +00:00
( PathBuf ::from ( LATEST_SESSION_REFERENCE ) , args )
2026-04-02 07:15:03 +00:00
}
2026-04-02 08:35:56 +00:00
Some ( first ) = > ( PathBuf ::from ( first ) , & args [ 1 .. ] ) ,
2026-04-02 07:15:03 +00:00
} ;
2026-04-02 07:37:25 +00:00
let mut commands = Vec ::new ( ) ;
let mut current_command = String ::new ( ) ;
2026-04-02 08:35:56 +00:00
for token in command_tokens {
2026-04-02 07:37:25 +00:00
if token . trim_start ( ) . starts_with ( '/' ) {
if resume_command_can_absorb_token ( & current_command , token ) {
current_command . push ( ' ' ) ;
current_command . push_str ( token ) ;
continue ;
}
if ! current_command . is_empty ( ) {
commands . push ( current_command ) ;
}
2026-04-02 17:52:31 +09:00
current_command = String ::from ( token . as_str ( ) ) ;
2026-04-02 07:37:25 +00:00
continue ;
}
if current_command . is_empty ( ) {
return Err ( " --resume trailing arguments must be slash commands " . to_string ( ) ) ;
}
current_command . push ( ' ' ) ;
current_command . push_str ( token ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-04-02 07:37:25 +00:00
if ! current_command . is_empty ( ) {
commands . push ( current_command ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-04-02 07:37:25 +00:00
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
Ok ( CliAction ::ResumeSession {
session_path ,
2026-03-31 20:00:13 +00:00
commands ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
} )
}
2026-04-02 07:37:25 +00:00
fn resume_command_can_absorb_token ( current_command : & str , token : & str ) -> bool {
matches! (
SlashCommand ::parse ( current_command ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Export { path : None } ) )
2026-04-02 07:37:25 +00:00
) & & ! looks_like_slash_command_token ( token )
}
fn looks_like_slash_command_token ( token : & str ) -> bool {
let trimmed = token . trim_start ( ) ;
let Some ( name ) = trimmed . strip_prefix ( '/' ) . and_then ( | value | {
value
. split_whitespace ( )
. next ( )
. map ( str ::trim )
. filter ( | value | ! value . is_empty ( ) )
} ) else {
return false ;
} ;
slash_command_specs ( )
. iter ( )
. any ( | spec | spec . name = = name | | spec . aliases . contains ( & name ) )
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn dump_manifests ( ) {
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
let workspace_dir = PathBuf ::from ( env! ( " CARGO_MANIFEST_DIR " ) ) . join ( " ../.. " ) ;
let paths = UpstreamPaths ::from_workspace_dir ( & workspace_dir ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
match extract_manifest ( & paths ) {
Ok ( manifest ) = > {
println! ( " commands: {} " , manifest . commands . entries ( ) . len ( ) ) ;
println! ( " tools: {} " , manifest . tools . entries ( ) . len ( ) ) ;
println! ( " bootstrap phases: {} " , manifest . bootstrap . phases ( ) . len ( ) ) ;
}
Err ( error ) = > {
eprintln! ( " failed to extract manifests: {error} " ) ;
std ::process ::exit ( 1 ) ;
}
}
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
}
fn print_bootstrap_plan ( ) {
2026-03-31 18:39:39 +00:00
for phase in runtime ::BootstrapPlan ::claude_code_default ( ) . phases ( ) {
feat: Rust port of Claw Code CLI
Crates:
- api: Anthropic Messages API client with SSE streaming
- tools: compatible tool implementations (Bash, Read, Write, Edit, Glob, Grep + extended suite)
- runtime: conversation loop, session persistence, permissions, system prompt builder
- claw-cli: terminal UI with markdown rendering, syntax highlighting, spinners
- commands: subcommand definitions
- compat-harness: upstream TS parity verification
All crates pass cargo fmt/clippy/test.
2026-03-31 17:43:09 +00:00
println! ( " - {phase:?} " ) ;
}
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
2026-04-01 03:20:26 +00:00
fn default_oauth_config ( ) -> OAuthConfig {
OAuthConfig {
client_id : String ::from ( " 9d1c250a-e61b-44d9-88ed-5944d1962f5e " ) ,
authorize_url : String ::from ( " https://platform.claude.com/oauth/authorize " ) ,
token_url : String ::from ( " https://platform.claude.com/v1/oauth/token " ) ,
callback_port : None ,
manual_redirect_url : None ,
scopes : vec ! [
String ::from ( " user:profile " ) ,
String ::from ( " user:inference " ) ,
String ::from ( " user:sessions:claude_code " ) ,
] ,
}
}
2026-03-31 23:38:05 +00:00
fn run_login ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let config = ConfigLoader ::default_for ( & cwd ) . load ( ) ? ;
2026-04-01 03:20:26 +00:00
let default_oauth = default_oauth_config ( ) ;
let oauth = config . oauth ( ) . unwrap_or ( & default_oauth ) ;
2026-03-31 23:38:05 +00:00
let callback_port = oauth . callback_port . unwrap_or ( DEFAULT_OAUTH_CALLBACK_PORT ) ;
let redirect_uri = runtime ::loopback_redirect_uri ( callback_port ) ;
let pkce = generate_pkce_pair ( ) ? ;
let state = generate_state ( ) ? ;
let authorize_url =
OAuthAuthorizationRequest ::from_config ( oauth , redirect_uri . clone ( ) , state . clone ( ) , & pkce )
. build_url ( ) ;
println! ( " Starting Claude OAuth login... " ) ;
println! ( " Listening for callback on {redirect_uri} " ) ;
if let Err ( error ) = open_browser ( & authorize_url ) {
eprintln! ( " warning: failed to open browser automatically: {error} " ) ;
println! ( " Open this URL manually: \n {authorize_url} " ) ;
}
let callback = wait_for_oauth_callback ( callback_port ) ? ;
if let Some ( error ) = callback . error {
let description = callback
. error_description
. unwrap_or_else ( | | " authorization failed " . to_string ( ) ) ;
return Err ( io ::Error ::other ( format! ( " {error} : {description} " ) ) . into ( ) ) ;
}
let code = callback . code . ok_or_else ( | | {
io ::Error ::new ( io ::ErrorKind ::InvalidData , " callback did not include code " )
} ) ? ;
let returned_state = callback . state . ok_or_else ( | | {
io ::Error ::new ( io ::ErrorKind ::InvalidData , " callback did not include state " )
} ) ? ;
if returned_state ! = state {
return Err ( io ::Error ::new ( io ::ErrorKind ::InvalidData , " oauth state mismatch " ) . into ( ) ) ;
}
2026-04-01 03:55:00 +00:00
let client = AnthropicClient ::from_auth ( AuthSource ::None ) . with_base_url ( api ::read_base_url ( ) ) ;
2026-03-31 23:38:05 +00:00
let exchange_request =
OAuthTokenExchangeRequest ::from_config ( oauth , code , state , pkce . verifier , redirect_uri ) ;
let runtime = tokio ::runtime ::Runtime ::new ( ) ? ;
let token_set = runtime . block_on ( client . exchange_oauth_code ( oauth , & exchange_request ) ) ? ;
save_oauth_credentials ( & runtime ::OAuthTokenSet {
access_token : token_set . access_token ,
refresh_token : token_set . refresh_token ,
expires_at : token_set . expires_at ,
scopes : token_set . scopes ,
} ) ? ;
println! ( " Claude OAuth login complete. " ) ;
Ok ( ( ) )
}
fn run_logout ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
clear_oauth_credentials ( ) ? ;
println! ( " Claude OAuth credentials cleared. " ) ;
Ok ( ( ) )
}
fn open_browser ( url : & str ) -> io ::Result < ( ) > {
let commands = if cfg! ( target_os = " macos " ) {
vec! [ ( " open " , vec! [ url ] ) ]
} else if cfg! ( target_os = " windows " ) {
vec! [ ( " cmd " , vec! [ " /C " , " start " , " " , url ] ) ]
} else {
vec! [ ( " xdg-open " , vec! [ url ] ) ]
} ;
for ( program , args ) in commands {
match Command ::new ( program ) . args ( args ) . spawn ( ) {
Ok ( _ ) = > return Ok ( ( ) ) ,
Err ( error ) if error . kind ( ) = = io ::ErrorKind ::NotFound = > { }
Err ( error ) = > return Err ( error ) ,
}
}
Err ( io ::Error ::new (
io ::ErrorKind ::NotFound ,
" no supported browser opener command found " ,
) )
}
fn wait_for_oauth_callback (
port : u16 ,
) -> Result < runtime ::OAuthCallbackParams , Box < dyn std ::error ::Error > > {
let listener = TcpListener ::bind ( ( " 127.0.0.1 " , port ) ) ? ;
let ( mut stream , _ ) = listener . accept ( ) ? ;
let mut buffer = [ 0_ u8 ; 4096 ] ;
let bytes_read = stream . read ( & mut buffer ) ? ;
let request = String ::from_utf8_lossy ( & buffer [ .. bytes_read ] ) ;
let request_line = request . lines ( ) . next ( ) . ok_or_else ( | | {
io ::Error ::new ( io ::ErrorKind ::InvalidData , " missing callback request line " )
} ) ? ;
let target = request_line . split_whitespace ( ) . nth ( 1 ) . ok_or_else ( | | {
io ::Error ::new (
io ::ErrorKind ::InvalidData ,
" missing callback request target " ,
)
} ) ? ;
let callback = parse_oauth_callback_request_target ( target )
. map_err ( | error | io ::Error ::new ( io ::ErrorKind ::InvalidData , error ) ) ? ;
let body = if callback . error . is_some ( ) {
" Claude OAuth login failed. You can close this window. "
} else {
" Claude OAuth login succeeded. You can close this window. "
} ;
let response = format! (
" HTTP/1.1 200 OK \r \n content-type: text/plain; charset=utf-8 \r \n content-length: {} \r \n connection: close \r \n \r \n {} " ,
body . len ( ) ,
body
) ;
stream . write_all ( response . as_bytes ( ) ) ? ;
Ok ( callback )
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
fn print_system_prompt ( cwd : PathBuf , date : String ) {
match load_system_prompt ( cwd , date , env ::consts ::OS , " unknown " ) {
Ok ( sections ) = > println! ( " {} " , sections . join ( " \n \n " ) ) ,
Err ( error ) = > {
eprintln! ( " failed to build system prompt: {error} " ) ;
std ::process ::exit ( 1 ) ;
}
}
}
2026-03-31 23:38:53 +00:00
fn print_version ( ) {
println! ( " {} " , render_version_report ( ) ) ;
}
2026-03-31 20:00:13 +00:00
fn resume_session ( session_path : & Path , commands : & [ String ] ) {
2026-04-01 06:15:14 +00:00
let resolved_path = if session_path . exists ( ) {
session_path . to_path_buf ( )
} else {
match resolve_session_reference ( & session_path . display ( ) . to_string ( ) ) {
Ok ( handle ) = > handle . path ,
Err ( error ) = > {
eprintln! ( " failed to restore session: {error} " ) ;
std ::process ::exit ( 1 ) ;
}
}
} ;
let session = match Session ::load_from_path ( & resolved_path ) {
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
Ok ( session ) = > session ,
Err ( error ) = > {
eprintln! ( " failed to restore session: {error} " ) ;
std ::process ::exit ( 1 ) ;
}
} ;
2026-03-31 20:00:13 +00:00
if commands . is_empty ( ) {
println! (
" Restored session from {} ({} messages). " ,
2026-04-01 06:15:14 +00:00
resolved_path . display ( ) ,
2026-03-31 20:00:13 +00:00
session . messages . len ( )
) ;
return ;
}
let mut session = session ;
for raw_command in commands {
2026-04-02 18:09:48 +09:00
let command = match SlashCommand ::parse ( raw_command ) {
Ok ( Some ( command ) ) = > command ,
Ok ( None ) = > {
eprintln! ( " unsupported resumed command: {raw_command} " ) ;
std ::process ::exit ( 2 ) ;
}
Err ( error ) = > {
eprintln! ( " {error} " ) ;
std ::process ::exit ( 2 ) ;
}
2026-03-31 20:00:13 +00:00
} ;
2026-04-01 06:15:14 +00:00
match run_resume_command ( & resolved_path , & session , & command ) {
2026-03-31 20:00:13 +00:00
Ok ( ResumeCommandOutcome {
session : next_session ,
message ,
} ) = > {
session = next_session ;
if let Some ( message ) = message {
println! ( " {message} " ) ;
}
}
2026-03-31 19:54:09 +00:00
Err ( error ) = > {
eprintln! ( " {error} " ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
std ::process ::exit ( 2 ) ;
}
}
}
}
2026-03-31 20:00:13 +00:00
#[ derive(Debug, Clone) ]
struct ResumeCommandOutcome {
session : Session ,
message : Option < String > ,
}
2026-03-31 20:22:59 +00:00
#[ derive(Debug, Clone) ]
struct StatusContext {
cwd : PathBuf ,
session_path : Option < PathBuf > ,
loaded_config_files : usize ,
discovered_config_files : usize ,
memory_file_count : usize ,
2026-03-31 21:06:51 +00:00
project_root : Option < PathBuf > ,
git_branch : Option < String > ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
git_summary : GitWorkspaceSummary ,
2026-04-01 01:14:38 +00:00
sandbox_status : runtime ::SandboxStatus ,
2026-03-31 20:22:59 +00:00
}
#[ derive(Debug, Clone, Copy) ]
struct StatusUsage {
message_count : usize ,
turns : u32 ,
latest : TokenUsage ,
cumulative : TokenUsage ,
estimated_tokens : usize ,
}
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
#[ allow(clippy::struct_field_names) ]
#[ derive(Debug, Clone, Copy, Default, PartialEq, Eq) ]
struct GitWorkspaceSummary {
changed_files : usize ,
staged_files : usize ,
unstaged_files : usize ,
untracked_files : usize ,
conflicted_files : usize ,
}
impl GitWorkspaceSummary {
fn is_clean ( self ) -> bool {
self . changed_files = = 0
}
fn headline ( self ) -> String {
if self . is_clean ( ) {
" clean " . to_string ( )
} else {
let mut details = Vec ::new ( ) ;
if self . staged_files > 0 {
details . push ( format! ( " {} staged " , self . staged_files ) ) ;
}
if self . unstaged_files > 0 {
details . push ( format! ( " {} unstaged " , self . unstaged_files ) ) ;
}
if self . untracked_files > 0 {
details . push ( format! ( " {} untracked " , self . untracked_files ) ) ;
}
if self . conflicted_files > 0 {
details . push ( format! ( " {} conflicted " , self . conflicted_files ) ) ;
}
format! (
" dirty · {} files · {} " ,
self . changed_files ,
details . join ( " , " )
)
}
}
}
2026-04-02 08:32:15 +00:00
#[ cfg(test) ]
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
fn format_unknown_slash_command_message ( name : & str ) -> String {
2026-04-02 08:30:05 +00:00
let suggestions = suggest_slash_commands ( name ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
if suggestions . is_empty ( ) {
format! ( " unknown slash command: / {name} . Use /help to list available commands. " )
} else {
format! (
" unknown slash command: /{name}. Did you mean {}? Use /help to list available commands. " ,
suggestions . join ( " , " )
)
}
}
2026-03-31 20:43:56 +00:00
fn format_model_report ( model : & str , message_count : usize , turns : u32 ) -> String {
format! (
" Model
Current model { model }
Session messages { message_count }
Session turns { turns }
Usage
Inspect current model with / model
Switch models with / model < name > "
)
}
fn format_model_switch_report ( previous : & str , next : & str , message_count : usize ) -> String {
format! (
" Model updated
Previous { previous }
Current { next }
Preserved msgs { message_count } "
)
}
2026-03-31 21:01:21 +00:00
fn format_permissions_report ( mode : & str ) -> String {
2026-03-31 22:19:58 +00:00
let modes = [
( " read-only " , " Read/search tools only " , mode = = " read-only " ) ,
(
" workspace-write " ,
" Edit files inside the workspace " ,
mode = = " workspace-write " ,
) ,
(
" danger-full-access " ,
" Unrestricted tool access " ,
mode = = " danger-full-access " ,
) ,
]
. into_iter ( )
. map ( | ( name , description , is_current ) | {
let marker = if is_current {
" ● current "
} else {
" ○ available "
} ;
format! ( " {name:<18} {marker:<11} {description} " )
} )
. collect ::< Vec < _ > > ( )
. join (
"
" ,
) ;
2026-03-31 21:01:21 +00:00
format! (
" Permissions
2026-03-31 22:19:58 +00:00
Active mode { mode }
Mode status live session default
2026-03-31 21:01:21 +00:00
2026-03-31 22:19:58 +00:00
Modes
{ modes }
Usage
Inspect current mode with / permissions
Switch modes with / permissions < mode > "
2026-03-31 21:01:21 +00:00
)
}
fn format_permissions_switch_report ( previous : & str , next : & str ) -> String {
format! (
" Permissions updated
2026-03-31 22:19:58 +00:00
Result mode switched
Previous mode { previous }
Active mode { next }
Applies to subsequent tool calls
Usage / permissions to inspect current mode "
2026-03-31 21:01:21 +00:00
)
}
2026-03-31 21:02:24 +00:00
fn format_cost_report ( usage : TokenUsage ) -> String {
format! (
" Cost
Input tokens { }
Output tokens { }
Cache create { }
Cache read { }
Total tokens { } " ,
usage . input_tokens ,
usage . output_tokens ,
usage . cache_creation_input_tokens ,
usage . cache_read_input_tokens ,
usage . total_tokens ( ) ,
)
}
2026-03-31 21:04:42 +00:00
fn format_resume_report ( session_path : & str , message_count : usize , turns : u32 ) -> String {
format! (
" Session resumed
Session file { session_path }
Messages { message_count }
Turns { turns } "
)
}
2026-04-02 07:15:03 +00:00
fn render_resume_usage ( ) -> String {
format! (
" Resume
Usage / resume < session - path | session - id | { LATEST_SESSION_REFERENCE } >
Auto - save . claw / sessions / < session - id > . { PRIMARY_SESSION_EXTENSION }
Tip use / session list to inspect saved sessions "
)
}
2026-03-31 21:15:37 +00:00
fn format_compact_report ( removed : usize , resulting_messages : usize , skipped : bool ) -> String {
if skipped {
format! (
" Compact
Result skipped
Reason session below compaction threshold
Messages kept { resulting_messages } "
)
} else {
format! (
" Compact
Result compacted
Messages removed { removed }
Messages kept { resulting_messages } "
)
}
}
2026-04-01 03:48:50 +00:00
fn format_auto_compaction_notice ( removed : usize ) -> String {
format! ( " [auto-compacted: removed {removed} messages] " )
}
2026-03-31 21:06:51 +00:00
fn parse_git_status_metadata ( status : Option < & str > ) -> ( Option < PathBuf > , Option < String > ) {
2026-04-01 01:10:57 +00:00
parse_git_status_metadata_for (
& env ::current_dir ( ) . unwrap_or_else ( | _ | PathBuf ::from ( " . " ) ) ,
status ,
)
}
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 ( ) )
}
}
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
fn parse_git_workspace_summary ( status : Option < & str > ) -> GitWorkspaceSummary {
let mut summary = GitWorkspaceSummary ::default ( ) ;
let Some ( status ) = status else {
return summary ;
} ;
for line in status . lines ( ) {
if line . starts_with ( " ## " ) | | line . trim ( ) . is_empty ( ) {
continue ;
}
summary . changed_files + = 1 ;
let mut chars = line . chars ( ) ;
let index_status = chars . next ( ) . unwrap_or ( ' ' ) ;
let worktree_status = chars . next ( ) . unwrap_or ( ' ' ) ;
if index_status = = '?' & & worktree_status = = '?' {
summary . untracked_files + = 1 ;
continue ;
}
if index_status ! = ' ' {
summary . staged_files + = 1 ;
}
if worktree_status ! = ' ' {
summary . unstaged_files + = 1 ;
}
if ( matches! ( index_status , 'U' | 'A' ) & & matches! ( worktree_status , 'U' | 'A' ) )
| | index_status = = 'U'
| | worktree_status = = 'U'
{
summary . conflicted_files + = 1 ;
}
}
summary
}
2026-04-01 01:10:57 +00:00
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 ( )
2026-03-31 21:06:51 +00:00
}
2026-04-01 01:10:57 +00:00
fn find_git_root_in ( cwd : & Path ) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
2026-03-31 21:06:51 +00:00
let output = std ::process ::Command ::new ( " git " )
. args ( [ " rev-parse " , " --show-toplevel " ] )
2026-04-01 01:10:57 +00:00
. current_dir ( cwd )
2026-03-31 21:06:51 +00:00
. output ( ) ? ;
if ! output . status . success ( ) {
return Err ( " not a git repository " . into ( ) ) ;
}
let path = String ::from_utf8 ( output . stdout ) ? . trim ( ) . to_string ( ) ;
if path . is_empty ( ) {
return Err ( " empty git root " . into ( ) ) ;
}
Ok ( PathBuf ::from ( path ) )
}
2026-04-01 01:10:57 +00:00
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 )
}
2026-03-31 22:49:50 +00:00
#[ allow(clippy::too_many_lines) ]
2026-03-31 19:54:09 +00:00
fn run_resume_command (
session_path : & Path ,
session : & Session ,
command : & SlashCommand ,
2026-03-31 20:00:13 +00:00
) -> Result < ResumeCommandOutcome , Box < dyn std ::error ::Error > > {
2026-03-31 19:54:09 +00:00
match command {
2026-03-31 20:00:13 +00:00
SlashCommand ::Help = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( render_repl_help ( ) ) ,
} ) ,
2026-03-31 19:54:09 +00:00
SlashCommand ::Compact = > {
2026-03-31 21:15:37 +00:00
let result = runtime ::compact_session (
2026-03-31 19:54:09 +00:00
session ,
CompactionConfig {
max_estimated_tokens : 0 ,
.. CompactionConfig ::default ( )
} ,
2026-03-31 21:15:37 +00:00
) ;
let removed = result . removed_message_count ;
let kept = result . compacted_session . messages . len ( ) ;
let skipped = removed = = 0 ;
result . compacted_session . save_to_path ( session_path ) ? ;
2026-03-31 20:00:13 +00:00
Ok ( ResumeCommandOutcome {
2026-03-31 21:15:37 +00:00
session : result . compacted_session ,
message : Some ( format_compact_report ( removed , kept , skipped ) ) ,
2026-03-31 20:00:13 +00:00
} )
}
2026-03-31 20:42:50 +00:00
SlashCommand ::Clear { confirm } = > {
if ! confirm {
return Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some (
" clear: confirmation required; rerun with /clear --confirm " . to_string ( ) ,
) ,
} ) ;
}
2026-04-02 10:03:07 +00:00
let backup_path = write_session_clear_backup ( session , session_path ) ? ;
let previous_session_id = session . session_id . clone ( ) ;
2026-03-31 20:00:13 +00:00
let cleared = Session ::new ( ) ;
2026-04-02 10:03:07 +00:00
let new_session_id = cleared . session_id . clone ( ) ;
2026-03-31 20:00:13 +00:00
cleared . save_to_path ( session_path ) ? ;
Ok ( ResumeCommandOutcome {
session : cleared ,
message : Some ( format! (
2026-04-02 10:03:07 +00:00
" Session cleared \n Mode resumed session reset \n Previous session {previous_session_id} \n Backup {} \n Resume previous claw --resume {} \n New session {new_session_id} \n Session file {} " ,
backup_path . display ( ) ,
backup_path . display ( ) ,
2026-03-31 20:00:13 +00:00
session_path . display ( )
) ) ,
} )
2026-03-31 19:54:09 +00:00
}
SlashCommand ::Status = > {
2026-03-31 20:00:13 +00:00
let tracker = UsageTracker ::from_session ( session ) ;
let usage = tracker . cumulative_usage ( ) ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
2026-03-31 20:22:59 +00:00
message : Some ( format_status_report (
2026-03-31 20:00:13 +00:00
" restored-session " ,
2026-03-31 20:22:59 +00:00
StatusUsage {
message_count : session . messages . len ( ) ,
turns : tracker . turns ( ) ,
latest : tracker . current_turn_usage ( ) ,
cumulative : usage ,
estimated_tokens : 0 ,
} ,
2026-04-01 00:06:15 +00:00
default_permission_mode ( ) . as_str ( ) ,
2026-03-31 20:22:59 +00:00
& status_context ( Some ( session_path ) ) ? ,
2026-03-31 20:00:13 +00:00
) ) ,
} )
2026-03-31 19:54:09 +00:00
}
2026-04-01 01:14:38 +00:00
SlashCommand ::Sandbox = > {
let cwd = env ::current_dir ( ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let runtime_config = loader . load ( ) ? ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( format_sandbox_report ( & resolve_sandbox_status (
runtime_config . sandbox ( ) ,
& cwd ,
) ) ) ,
} )
}
2026-03-31 19:54:09 +00:00
SlashCommand ::Cost = > {
let usage = UsageTracker ::from_session ( session ) . cumulative_usage ( ) ;
2026-03-31 20:00:13 +00:00
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
2026-03-31 21:02:24 +00:00
message : Some ( format_cost_report ( usage ) ) ,
2026-03-31 20:00:13 +00:00
} )
2026-03-31 19:54:09 +00:00
}
2026-03-31 21:11:57 +00:00
SlashCommand ::Config { section } = > Ok ( ResumeCommandOutcome {
2026-03-31 20:00:13 +00:00
session : session . clone ( ) ,
2026-03-31 21:11:57 +00:00
message : Some ( render_config_report ( section . as_deref ( ) ) ? ) ,
2026-03-31 20:00:13 +00:00
} ) ,
2026-04-02 10:04:40 +00:00
SlashCommand ::Mcp { action , target } = > {
let cwd = env ::current_dir ( ) ? ;
let args = match ( action . as_deref ( ) , target . as_deref ( ) ) {
( None , None ) = > None ,
( Some ( action ) , None ) = > Some ( action . to_string ( ) ) ,
( Some ( action ) , Some ( target ) ) = > Some ( format! ( " {action} {target} " ) ) ,
( None , Some ( target ) ) = > Some ( target . to_string ( ) ) ,
} ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( handle_mcp_slash_command ( args . as_deref ( ) , & cwd ) ? ) ,
} )
}
2026-03-31 20:00:13 +00:00
SlashCommand ::Memory = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( render_memory_report ( ) ? ) ,
} ) ,
SlashCommand ::Init = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( init_claude_md ( ) ? ) ,
} ) ,
2026-03-31 22:49:50 +00:00
SlashCommand ::Diff = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
2026-04-01 01:10:57 +00:00
message : Some ( render_diff_report_for (
session_path . parent ( ) . unwrap_or_else ( | | Path ::new ( " . " ) ) ,
) ? ) ,
2026-03-31 22:49:50 +00:00
} ) ,
SlashCommand ::Version = > Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( render_version_report ( ) ) ,
} ) ,
SlashCommand ::Export { path } = > {
let export_path = resolve_export_path ( path . as_deref ( ) , session ) ? ;
fs ::write ( & export_path , render_export_text ( session ) ) ? ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( format! (
" Export \n Result wrote transcript \n File {} \n Messages {} " ,
export_path . display ( ) ,
session . messages . len ( ) ,
) ) ,
} )
}
2026-04-01 08:30:02 +00:00
SlashCommand ::Agents { args } = > {
let cwd = env ::current_dir ( ) ? ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( handle_agents_slash_command ( args . as_deref ( ) , & cwd ) ? ) ,
} )
}
SlashCommand ::Skills { args } = > {
let cwd = env ::current_dir ( ) ? ;
Ok ( ResumeCommandOutcome {
session : session . clone ( ) ,
message : Some ( handle_skills_slash_command ( args . as_deref ( ) , & cwd ) ? ) ,
} )
}
2026-04-02 07:15:03 +00:00
SlashCommand ::Unknown ( name ) = > Err ( format_unknown_slash_command ( name ) . into ( ) ) ,
2026-04-01 03:55:00 +00:00
SlashCommand ::Bughunter { .. }
2026-04-02 18:10:32 +09:00
| SlashCommand ::Commit { .. }
2026-04-01 18:57:50 +09:00
| SlashCommand ::Pr { .. }
| SlashCommand ::Issue { .. }
| SlashCommand ::Ultraplan { .. }
| SlashCommand ::Teleport { .. }
2026-04-02 18:10:32 +09:00
| SlashCommand ::DebugToolCall { .. }
2026-04-01 03:55:00 +00:00
| SlashCommand ::Resume { .. }
2026-03-31 19:54:09 +00:00
| SlashCommand ::Model { .. }
| SlashCommand ::Permissions { .. }
2026-03-31 22:49:50 +00:00
| SlashCommand ::Session { .. }
feat: add 40 slash commands — command surface 67/141
Port 40 missing user-facing slash commands from upstream parity audit:
Session: /doctor, /login, /logout, /usage, /stats, /rename, /privacy-settings
Workspace: /branch, /add-dir, /files, /hooks, /release-notes
Discovery: /context, /tasks, /doctor, /ide, /desktop
Analysis: /review, /security-review, /advisor, /insights
Appearance: /theme, /vim, /voice, /color, /effort, /fast, /brief,
/output-style, /keybindings, /stickers
Communication: /copy, /share, /feedback, /summary, /tag, /thinkback,
/plan, /exit, /upgrade, /rewind
All commands have full SlashCommandSpec, enum variant, parse arm,
and stub handler. Category system expanded with two new categories.
Tests updated for new counts (67 specs, 39 resume-supported).
fmt/clippy/tests all green.
2026-04-03 08:09:14 +09:00
| SlashCommand ::Plugins { .. }
| SlashCommand ::Doctor
| SlashCommand ::Login
| SlashCommand ::Logout
| SlashCommand ::Vim
| SlashCommand ::Upgrade
| SlashCommand ::Stats
| SlashCommand ::Share
| SlashCommand ::Feedback
| SlashCommand ::Files
| SlashCommand ::Fast
| SlashCommand ::Exit
| SlashCommand ::Summary
| SlashCommand ::Desktop
| SlashCommand ::Brief
| SlashCommand ::Advisor
| SlashCommand ::Stickers
| SlashCommand ::Insights
| SlashCommand ::Thinkback
| SlashCommand ::ReleaseNotes
| SlashCommand ::SecurityReview
| SlashCommand ::Keybindings
| SlashCommand ::PrivacySettings
| SlashCommand ::Plan { .. }
| SlashCommand ::Review { .. }
| SlashCommand ::Tasks { .. }
| SlashCommand ::Theme { .. }
| SlashCommand ::Voice { .. }
| SlashCommand ::Usage { .. }
| SlashCommand ::Rename { .. }
| SlashCommand ::Copy { .. }
| SlashCommand ::Hooks { .. }
| SlashCommand ::Context { .. }
| SlashCommand ::Color { .. }
| SlashCommand ::Effort { .. }
| SlashCommand ::Branch { .. }
| SlashCommand ::Rewind { .. }
| SlashCommand ::Ide { .. }
| SlashCommand ::Tag { .. }
| SlashCommand ::OutputStyle { .. }
| SlashCommand ::AddDir { .. } = > Err ( " unsupported resumed slash command " . into ( ) ) ,
2026-03-31 19:54:09 +00:00
}
}
2026-03-31 23:38:53 +00:00
fn run_repl (
model : String ,
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 00:06:15 +00:00
permission_mode : PermissionMode ,
2026-03-31 23:38:53 +00:00
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-01 00:06:15 +00:00
let mut cli = LiveCli ::new ( model , true , allowed_tools , permission_mode ) ? ;
2026-04-02 07:19:14 +00:00
let mut editor =
input ::LineEditor ::new ( " > " , cli . repl_completion_candidates ( ) . unwrap_or_default ( ) ) ;
2026-03-31 22:49:50 +00:00
println! ( " {} " , cli . startup_banner ( ) ) ;
2026-03-31 18:39:39 +00:00
2026-04-01 00:14:38 +00:00
loop {
2026-04-02 07:19:14 +00:00
editor . set_completions ( cli . repl_completion_candidates ( ) . unwrap_or_default ( ) ) ;
2026-04-01 00:14:38 +00:00
match editor . read_line ( ) ? {
input ::ReadOutcome ::Submit ( input ) = > {
let trimmed = input . trim ( ) . to_string ( ) ;
if trimmed . is_empty ( ) {
continue ;
}
if matches! ( trimmed . as_str ( ) , " /exit " | " /quit " ) {
cli . persist_session ( ) ? ;
break ;
}
2026-04-02 18:09:48 +09:00
match SlashCommand ::parse ( & trimmed ) {
Ok ( Some ( command ) ) = > {
if cli . handle_repl_command ( command ) ? {
cli . persist_session ( ) ? ;
}
continue ;
}
Ok ( None ) = > { }
Err ( error ) = > {
eprintln! ( " {error} " ) ;
continue ;
2026-04-01 00:14:38 +00:00
}
}
editor . push_history ( input ) ;
cli . run_turn ( & trimmed ) ? ;
}
input ::ReadOutcome ::Cancel = > { }
input ::ReadOutcome ::Exit = > {
cli . persist_session ( ) ? ;
break ;
}
2026-03-31 18:39:39 +00:00
}
}
Ok ( ( ) )
}
2026-03-31 22:49:50 +00:00
#[ derive(Debug, Clone) ]
struct SessionHandle {
id : String ,
path : PathBuf ,
}
#[ derive(Debug, Clone) ]
struct ManagedSessionSummary {
id : String ,
path : PathBuf ,
2026-04-02 07:15:03 +00:00
modified_epoch_millis : u128 ,
2026-03-31 22:49:50 +00:00
message_count : usize ,
2026-04-01 06:15:14 +00:00
parent_session_id : Option < String > ,
branch_name : Option < String > ,
2026-03-31 22:49:50 +00:00
}
2026-03-31 18:39:39 +00:00
struct LiveCli {
model : String ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 00:06:15 +00:00
permission_mode : PermissionMode ,
2026-03-31 18:39:39 +00:00
system_prompt : Vec < String > ,
2026-04-02 10:04:54 +00:00
runtime : BuiltRuntime ,
2026-03-31 22:49:50 +00:00
session : SessionHandle ,
2026-03-31 18:39:39 +00:00
}
2026-04-02 10:04:54 +00:00
struct RuntimePluginState {
feature_config : runtime ::RuntimeFeatureConfig ,
tool_registry : GlobalToolRegistry ,
plugin_registry : PluginRegistry ,
2026-04-03 14:31:25 +00:00
mcp_state : Option < Arc < Mutex < RuntimeMcpState > > > ,
}
struct RuntimeMcpState {
runtime : tokio ::runtime ::Runtime ,
manager : McpServerManager ,
pending_servers : Vec < String > ,
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
degraded_report : Option < runtime ::McpDegradedReport > ,
2026-04-02 10:04:54 +00:00
}
struct BuiltRuntime {
runtime : Option < ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > > ,
plugin_registry : PluginRegistry ,
plugins_active : bool ,
2026-04-03 14:31:25 +00:00
mcp_state : Option < Arc < Mutex < RuntimeMcpState > > > ,
mcp_active : bool ,
2026-04-02 10:04:54 +00:00
}
impl BuiltRuntime {
fn new (
runtime : ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > ,
plugin_registry : PluginRegistry ,
2026-04-03 14:31:25 +00:00
mcp_state : Option < Arc < Mutex < RuntimeMcpState > > > ,
2026-04-02 10:04:54 +00:00
) -> Self {
Self {
runtime : Some ( runtime ) ,
plugin_registry ,
plugins_active : true ,
2026-04-03 14:31:25 +00:00
mcp_state ,
mcp_active : true ,
2026-04-02 10:04:54 +00:00
}
}
fn with_hook_abort_signal ( mut self , hook_abort_signal : runtime ::HookAbortSignal ) -> Self {
let runtime = self
. runtime
. take ( )
. expect ( " runtime should exist before installing hook abort signal " ) ;
self . runtime = Some ( runtime . with_hook_abort_signal ( hook_abort_signal ) ) ;
self
}
fn shutdown_plugins ( & mut self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
if self . plugins_active {
self . plugin_registry . shutdown ( ) ? ;
self . plugins_active = false ;
}
Ok ( ( ) )
}
2026-04-03 14:31:25 +00:00
fn shutdown_mcp ( & mut self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
if self . mcp_active {
if let Some ( mcp_state ) = & self . mcp_state {
mcp_state
. lock ( )
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
. shutdown ( ) ? ;
}
self . mcp_active = false ;
}
Ok ( ( ) )
}
2026-04-02 10:04:54 +00:00
}
impl Deref for BuiltRuntime {
type Target = ConversationRuntime < AnthropicRuntimeClient , CliToolExecutor > ;
fn deref ( & self ) -> & Self ::Target {
self . runtime
. as_ref ( )
. expect ( " runtime should exist while built runtime is alive " )
}
}
impl DerefMut for BuiltRuntime {
fn deref_mut ( & mut self ) -> & mut Self ::Target {
self . runtime
. as_mut ( )
. expect ( " runtime should exist while built runtime is alive " )
}
}
impl Drop for BuiltRuntime {
fn drop ( & mut self ) {
2026-04-03 14:31:25 +00:00
let _ = self . shutdown_mcp ( ) ;
2026-04-02 10:04:54 +00:00
let _ = self . shutdown_plugins ( ) ;
}
}
2026-04-03 14:31:25 +00:00
#[ derive(Debug, Deserialize) ]
struct ToolSearchRequest {
query : String ,
max_results : Option < usize > ,
}
#[ derive(Debug, Deserialize) ]
struct McpToolRequest {
#[ serde(rename = " qualifiedName " ) ]
qualified_name : Option < String > ,
tool : Option < String > ,
arguments : Option < serde_json ::Value > ,
}
#[ derive(Debug, Deserialize) ]
struct ListMcpResourcesRequest {
server : Option < String > ,
}
#[ derive(Debug, Deserialize) ]
struct ReadMcpResourceRequest {
server : String ,
uri : String ,
}
impl RuntimeMcpState {
fn new (
runtime_config : & runtime ::RuntimeConfig ,
) -> Result < Option < ( Self , runtime ::McpToolDiscoveryReport ) > , Box < dyn std ::error ::Error > > {
let mut manager = McpServerManager ::from_runtime_config ( runtime_config ) ;
if manager . server_names ( ) . is_empty ( ) & & manager . unsupported_servers ( ) . is_empty ( ) {
return Ok ( None ) ;
}
let runtime = tokio ::runtime ::Runtime ::new ( ) ? ;
let discovery = runtime . block_on ( manager . discover_tools_best_effort ( ) ) ;
let pending_servers = discovery
. failed_servers
. iter ( )
. map ( | failure | failure . server_name . clone ( ) )
. chain (
discovery
. unsupported_servers
. iter ( )
. map ( | server | server . server_name . clone ( ) ) ,
)
. collect ::< BTreeSet < _ > > ( )
. into_iter ( )
. collect ::< Vec < _ > > ( ) ;
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
let available_tools = discovery
. tools
. iter ( )
. map ( | tool | tool . qualified_name . clone ( ) )
. collect ::< Vec < _ > > ( ) ;
let failed_server_names = pending_servers . iter ( ) . cloned ( ) . collect ::< BTreeSet < _ > > ( ) ;
let working_servers = manager
. server_names ( )
. into_iter ( )
. filter ( | server_name | ! failed_server_names . contains ( server_name ) )
. collect ::< Vec < _ > > ( ) ;
let failed_servers = discovery
. failed_servers
. iter ( )
. map ( | failure | runtime ::McpFailedServer {
server_name : failure . server_name . clone ( ) ,
phase : runtime ::McpLifecyclePhase ::ToolDiscovery ,
error : runtime ::McpErrorSurface ::new (
runtime ::McpLifecyclePhase ::ToolDiscovery ,
Some ( failure . server_name . clone ( ) ) ,
failure . error . clone ( ) ,
std ::collections ::BTreeMap ::new ( ) ,
true ,
) ,
} )
. chain ( discovery . unsupported_servers . iter ( ) . map ( | server | {
runtime ::McpFailedServer {
server_name : server . server_name . clone ( ) ,
phase : runtime ::McpLifecyclePhase ::ServerRegistration ,
error : runtime ::McpErrorSurface ::new (
runtime ::McpLifecyclePhase ::ServerRegistration ,
Some ( server . server_name . clone ( ) ) ,
server . reason . clone ( ) ,
std ::collections ::BTreeMap ::from ( [ (
" transport " . to_string ( ) ,
format! ( " {:?} " , server . transport ) . to_ascii_lowercase ( ) ,
) ] ) ,
false ,
) ,
}
} ) )
. collect ::< Vec < _ > > ( ) ;
let degraded_report = ( ! failed_servers . is_empty ( ) ) . then ( | | {
runtime ::McpDegradedReport ::new (
working_servers ,
failed_servers ,
available_tools . clone ( ) ,
available_tools ,
)
} ) ;
2026-04-03 14:31:25 +00:00
Ok ( Some ( (
Self {
runtime ,
manager ,
pending_servers ,
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
degraded_report ,
2026-04-03 14:31:25 +00:00
} ,
discovery ,
) ) )
}
fn shutdown ( & mut self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
self . runtime . block_on ( self . manager . shutdown ( ) ) ? ;
Ok ( ( ) )
}
fn pending_servers ( & self ) -> Option < Vec < String > > {
( ! self . pending_servers . is_empty ( ) ) . then ( | | self . pending_servers . clone ( ) )
}
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
fn degraded_report ( & self ) -> Option < runtime ::McpDegradedReport > {
self . degraded_report . clone ( )
}
2026-04-03 14:31:25 +00:00
fn server_names ( & self ) -> Vec < String > {
self . manager . server_names ( )
}
fn call_tool (
& mut self ,
qualified_tool_name : & str ,
arguments : Option < serde_json ::Value > ,
) -> Result < String , ToolError > {
let response = self
. runtime
. block_on ( self . manager . call_tool ( qualified_tool_name , arguments ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) ) ? ;
if let Some ( error ) = response . error {
return Err ( ToolError ::new ( format! (
" MCP tool `{qualified_tool_name}` returned JSON-RPC error: {} ({}) " ,
error . message , error . code
) ) ) ;
}
let result = response . result . ok_or_else ( | | {
ToolError ::new ( format! (
" MCP tool `{qualified_tool_name}` returned no result payload "
) )
} ) ? ;
serde_json ::to_string_pretty ( & result ) . map_err ( | error | ToolError ::new ( error . to_string ( ) ) )
}
fn list_resources_for_server ( & mut self , server_name : & str ) -> Result < String , ToolError > {
let result = self
. runtime
. block_on ( self . manager . list_resources ( server_name ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) ) ? ;
serde_json ::to_string_pretty ( & json! ( {
" server " : server_name ,
" resources " : result . resources ,
} ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) )
}
fn list_resources_for_all_servers ( & mut self ) -> Result < String , ToolError > {
let mut resources = Vec ::new ( ) ;
let mut failures = Vec ::new ( ) ;
for server_name in self . server_names ( ) {
match self
. runtime
. block_on ( self . manager . list_resources ( & server_name ) )
{
Ok ( result ) = > resources . push ( json! ( {
" server " : server_name ,
" resources " : result . resources ,
} ) ) ,
Err ( error ) = > failures . push ( json! ( {
" server " : server_name ,
" error " : error . to_string ( ) ,
} ) ) ,
}
}
if resources . is_empty ( ) & & ! failures . is_empty ( ) {
let message = failures
. iter ( )
. filter_map ( | failure | failure . get ( " error " ) . and_then ( serde_json ::Value ::as_str ) )
. collect ::< Vec < _ > > ( )
. join ( " ; " ) ;
return Err ( ToolError ::new ( message ) ) ;
}
serde_json ::to_string_pretty ( & json! ( {
" resources " : resources ,
" failures " : failures ,
} ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) )
}
fn read_resource ( & mut self , server_name : & str , uri : & str ) -> Result < String , ToolError > {
let result = self
. runtime
. block_on ( self . manager . read_resource ( server_name , uri ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) ) ? ;
serde_json ::to_string_pretty ( & json! ( {
" server " : server_name ,
" contents " : result . contents ,
} ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) )
}
}
fn build_runtime_mcp_state (
runtime_config : & runtime ::RuntimeConfig ,
) -> Result <
(
Option < Arc < Mutex < RuntimeMcpState > > > ,
Vec < RuntimeToolDefinition > ,
) ,
Box < dyn std ::error ::Error > ,
> {
let Some ( ( mcp_state , discovery ) ) = RuntimeMcpState ::new ( runtime_config ) ? else {
return Ok ( ( None , Vec ::new ( ) ) ) ;
} ;
let mut runtime_tools = discovery
. tools
. iter ( )
. map ( mcp_runtime_tool_definition )
. collect ::< Vec < _ > > ( ) ;
if ! mcp_state . server_names ( ) . is_empty ( ) {
runtime_tools . extend ( mcp_wrapper_tool_definitions ( ) ) ;
}
Ok ( ( Some ( Arc ::new ( Mutex ::new ( mcp_state ) ) ) , runtime_tools ) )
}
fn mcp_runtime_tool_definition ( tool : & runtime ::ManagedMcpTool ) -> RuntimeToolDefinition {
RuntimeToolDefinition {
name : tool . qualified_name . clone ( ) ,
description : Some (
tool . tool
. description
. clone ( )
. unwrap_or_else ( | | format! ( " Invoke MCP tool ` {} `. " , tool . qualified_name ) ) ,
) ,
input_schema : tool
. tool
. input_schema
. clone ( )
. unwrap_or_else ( | | json! ( { " type " : " object " , " additionalProperties " : true } ) ) ,
required_permission : permission_mode_for_mcp_tool ( & tool . tool ) ,
}
}
fn mcp_wrapper_tool_definitions ( ) -> Vec < RuntimeToolDefinition > {
vec! [
RuntimeToolDefinition {
name : " MCPTool " . to_string ( ) ,
description : Some (
" Call a configured MCP tool by its qualified name and JSON arguments. " . to_string ( ) ,
) ,
input_schema : json ! ( {
" type " : " object " ,
" properties " : {
" qualifiedName " : { " type " : " string " } ,
" arguments " : { }
} ,
" required " : [ " qualifiedName " ] ,
" additionalProperties " : false
} ) ,
required_permission : PermissionMode ::DangerFullAccess ,
} ,
RuntimeToolDefinition {
name : " ListMcpResourcesTool " . to_string ( ) ,
description : Some (
" List MCP resources from one configured server or from every connected server. "
. to_string ( ) ,
) ,
input_schema : json ! ( {
" type " : " object " ,
" properties " : {
" server " : { " type " : " string " }
} ,
" additionalProperties " : false
} ) ,
required_permission : PermissionMode ::ReadOnly ,
} ,
RuntimeToolDefinition {
name : " ReadMcpResourceTool " . to_string ( ) ,
description : Some ( " Read a specific MCP resource from a configured server. " . to_string ( ) ) ,
input_schema : json ! ( {
" type " : " object " ,
" properties " : {
" server " : { " type " : " string " } ,
" uri " : { " type " : " string " }
} ,
" required " : [ " server " , " uri " ] ,
" additionalProperties " : false
} ) ,
required_permission : PermissionMode ::ReadOnly ,
} ,
]
}
fn permission_mode_for_mcp_tool ( tool : & McpTool ) -> PermissionMode {
let read_only = mcp_annotation_flag ( tool , " readOnlyHint " ) ;
let destructive = mcp_annotation_flag ( tool , " destructiveHint " ) ;
let open_world = mcp_annotation_flag ( tool , " openWorldHint " ) ;
if read_only & & ! destructive & & ! open_world {
PermissionMode ::ReadOnly
} else if destructive | | open_world {
PermissionMode ::DangerFullAccess
} else {
PermissionMode ::WorkspaceWrite
}
}
fn mcp_annotation_flag ( tool : & McpTool , key : & str ) -> bool {
tool . annotations
. as_ref ( )
. and_then ( | annotations | annotations . get ( key ) )
. and_then ( serde_json ::Value ::as_bool )
. unwrap_or ( false )
}
2026-04-01 05:55:24 +00:00
struct HookAbortMonitor {
stop_tx : Option < Sender < ( ) > > ,
join_handle : Option < JoinHandle < ( ) > > ,
}
impl HookAbortMonitor {
fn spawn ( abort_signal : runtime ::HookAbortSignal ) -> Self {
Self ::spawn_with_waiter ( abort_signal , move | stop_rx , abort_signal | {
let Ok ( runtime ) = tokio ::runtime ::Builder ::new_current_thread ( )
. enable_all ( )
. build ( )
else {
return ;
} ;
runtime . block_on ( async move {
let wait_for_stop = tokio ::task ::spawn_blocking ( move | | {
let _ = stop_rx . recv ( ) ;
} ) ;
tokio ::select! {
result = tokio ::signal ::ctrl_c ( ) = > {
if result . is_ok ( ) {
abort_signal . abort ( ) ;
}
}
_ = wait_for_stop = > { }
}
} ) ;
} )
}
fn spawn_with_waiter < F > ( abort_signal : runtime ::HookAbortSignal , wait_for_interrupt : F ) -> Self
where
F : FnOnce ( Receiver < ( ) > , runtime ::HookAbortSignal ) + Send + 'static ,
{
let ( stop_tx , stop_rx ) = mpsc ::channel ( ) ;
let join_handle = thread ::spawn ( move | | wait_for_interrupt ( stop_rx , abort_signal ) ) ;
Self {
stop_tx : Some ( stop_tx ) ,
join_handle : Some ( join_handle ) ,
}
}
fn stop ( mut self ) {
if let Some ( stop_tx ) = self . stop_tx . take ( ) {
let _ = stop_tx . send ( ( ) ) ;
}
if let Some ( join_handle ) = self . join_handle . take ( ) {
let _ = join_handle . join ( ) ;
}
}
}
2026-03-31 18:39:39 +00:00
impl LiveCli {
2026-03-31 23:38:53 +00:00
fn new (
model : String ,
enable_tools : bool ,
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 00:06:15 +00:00
permission_mode : PermissionMode ,
2026-03-31 23:38:53 +00:00
) -> Result < Self , Box < dyn std ::error ::Error > > {
2026-03-31 18:39:39 +00:00
let system_prompt = build_system_prompt ( ) ? ;
2026-04-01 04:30:27 +00:00
let session_state = Session ::new ( ) ;
let session = create_managed_session_handle ( & session_state . session_id ) ? ;
2026-03-31 18:39:39 +00:00
let runtime = build_runtime (
2026-04-01 04:30:27 +00:00
session_state . with_persistence_path ( session . path . clone ( ) ) ,
2026-04-02 11:16:56 +09:00
& session . id ,
2026-03-31 18:39:39 +00:00
model . clone ( ) ,
system_prompt . clone ( ) ,
enable_tools ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 23:38:53 +00:00
allowed_tools . clone ( ) ,
2026-04-01 00:06:15 +00:00
permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-03-31 18:39:39 +00:00
) ? ;
2026-03-31 22:49:50 +00:00
let cli = Self {
2026-03-31 18:39:39 +00:00
model ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-04-01 00:06:15 +00:00
permission_mode ,
2026-03-31 18:39:39 +00:00
system_prompt ,
runtime ,
2026-03-31 22:49:50 +00:00
session ,
} ;
cli . persist_session ( ) ? ;
Ok ( cli )
}
fn startup_banner ( & self ) -> String {
2026-04-01 01:44:55 +00:00
let cwd = env ::current_dir ( ) . map_or_else (
| _ | " <unknown> " . to_string ( ) ,
| path | path . display ( ) . to_string ( ) ,
) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
let status = status_context ( None ) . ok ( ) ;
let git_branch = status
. as_ref ( )
. and_then ( | context | context . git_branch . as_deref ( ) )
. unwrap_or ( " unknown " ) ;
let workspace = status . as_ref ( ) . map_or_else (
| | " unknown " . to_string ( ) ,
| context | context . git_summary . headline ( ) ,
) ;
2026-04-02 07:15:03 +00:00
let session_path = self . session . path . strip_prefix ( Path ::new ( & cwd ) ) . map_or_else (
| _ | self . session . path . display ( ) . to_string ( ) ,
| path | path . display ( ) . to_string ( ) ,
) ;
2026-03-31 22:49:50 +00:00
format! (
2026-04-01 01:44:55 +00:00
" \x1b [38;5;196m \
█ █ █ █ █ █ ╗ █ █ ╗ █ █ █ █ █ ╗ █ █ ╗ █ █ ╗ \ n \
█ █ ╔ ═ ═ ═ ═ ╝ █ █ ║ █ █ ╔ ═ ═ █ █ ╗ █ █ ║ █ █ ║ \ n \
█ █ ║ █ █ ║ █ █ █ █ █ █ █ ║ █ █ ║ █ ╗ █ █ ║ \ n \
█ █ ║ █ █ ║ █ █ ╔ ═ ═ █ █ ║ █ █ ║ █ █ █ ╗ █ █ ║ \ n \
╚ █ █ █ █ █ █ ╗ █ █ █ █ █ █ █ ╗ █ █ ║ █ █ ║ ╚ █ █ █ ╔ █ █ █ ╔ ╝ \ n \
╚ ═ ═ ═ ═ ═ ╝ ╚ ═ ═ ═ ═ ═ ═ ╝ ╚ ═ ╝ ╚ ═ ╝ ╚ ═ ═ ╝ ╚ ═ ═ ╝ \ x1b [ 0 m \ x1b [ 38 ; 5 ; 208 mCode \ x1b [ 0 m 🦞 \ n \ n \
\ x1b [ 2 mModel \ x1b [ 0 m { } \ n \
\ x1b [ 2 mPermissions \ x1b [ 0 m { } \ n \
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
\ x1b [ 2 mBranch \ x1b [ 0 m { } \ n \
\ x1b [ 2 mWorkspace \ x1b [ 0 m { } \ n \
2026-04-01 01:44:55 +00:00
\ x1b [ 2 mDirectory \ x1b [ 0 m { } \ n \
2026-04-02 07:15:03 +00:00
\ x1b [ 2 mSession \ x1b [ 0 m { } \ n \
\ x1b [ 2 mAuto - save \ x1b [ 0 m { } \ n \ n \
2026-04-02 08:30:05 +00:00
Type \ x1b [ 1 m / help \ x1b [ 0 m for commands · \ x1b [ 1 m / status \ x1b [ 0 m for live context · \ x1b [ 2 m / resume latest \ x1b [ 0 m jumps back to the newest session · \ x1b [ 1 m / diff \ x1b [ 0 m then \ x1b [ 1 m / commit \ x1b [ 0 m to ship · \ x1b [ 2 mTab \ x1b [ 0 m for workflow completions · \ x1b [ 2 mShift + Enter \ x1b [ 0 m for newline " ,
2026-03-31 22:49:50 +00:00
self . model ,
2026-04-01 00:06:15 +00:00
self . permission_mode . as_str ( ) ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
git_branch ,
workspace ,
2026-04-01 01:44:55 +00:00
cwd ,
2026-03-31 22:49:50 +00:00
self . session . id ,
2026-04-02 07:15:03 +00:00
session_path ,
2026-03-31 22:49:50 +00:00
)
2026-03-31 18:39:39 +00:00
}
2026-04-02 07:19:14 +00:00
fn repl_completion_candidates ( & self ) -> Result < Vec < String > , Box < dyn std ::error ::Error > > {
Ok ( slash_command_completion_candidates_with_sessions (
& self . model ,
Some ( & self . session . id ) ,
list_managed_sessions ( ) ?
. into_iter ( )
. map ( | session | session . id )
. collect ( ) ,
) )
}
2026-04-01 05:58:00 +00:00
fn prepare_turn_runtime (
& self ,
emit_output : bool ,
2026-04-02 10:04:54 +00:00
) -> Result < ( BuiltRuntime , HookAbortMonitor ) , Box < dyn std ::error ::Error > > {
2026-04-01 05:55:24 +00:00
let hook_abort_signal = runtime ::HookAbortSignal ::new ( ) ;
2026-04-01 05:58:00 +00:00
let runtime = build_runtime (
self . runtime . session ( ) . clone ( ) ,
2026-04-02 11:16:56 +09:00
& self . session . id ,
2026-04-01 05:55:24 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-04-01 05:58:00 +00:00
emit_output ,
2026-04-01 05:55:24 +00:00
self . allowed_tools . clone ( ) ,
self . permission_mode ,
2026-04-02 11:05:03 +09:00
None ,
2026-04-01 05:55:24 +00:00
) ?
. with_hook_abort_signal ( hook_abort_signal . clone ( ) ) ;
let hook_abort_monitor = HookAbortMonitor ::spawn ( hook_abort_signal ) ;
2026-04-01 05:58:00 +00:00
Ok ( ( runtime , hook_abort_monitor ) )
}
2026-04-02 10:04:54 +00:00
fn replace_runtime ( & mut self , runtime : BuiltRuntime ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
self . runtime . shutdown_plugins ( ) ? ;
self . runtime = runtime ;
Ok ( ( ) )
}
2026-03-31 18:39:39 +00:00
fn run_turn ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-01 05:58:00 +00:00
let ( mut runtime , hook_abort_monitor ) = self . prepare_turn_runtime ( true ) ? ;
2026-03-31 18:39:39 +00:00
let mut spinner = Spinner ::new ( ) ;
let mut stdout = io ::stdout ( ) ;
spinner . tick (
2026-04-01 01:44:55 +00:00
" 🦀 Thinking... " ,
2026-03-31 18:39:39 +00:00
TerminalRenderer ::new ( ) . color_theme ( ) ,
& mut stdout ,
) ? ;
2026-04-01 00:06:15 +00:00
let mut permission_prompter = CliPermissionPrompter ::new ( self . permission_mode ) ;
2026-04-01 05:55:24 +00:00
let result = runtime . run_turn ( input , Some ( & mut permission_prompter ) ) ;
hook_abort_monitor . stop ( ) ;
2026-03-31 18:39:39 +00:00
match result {
2026-04-01 03:48:50 +00:00
Ok ( summary ) = > {
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-03-31 18:39:39 +00:00
spinner . finish (
2026-04-01 01:44:55 +00:00
" ✨ Done " ,
2026-03-31 18:39:39 +00:00
TerminalRenderer ::new ( ) . color_theme ( ) ,
& mut stdout ,
) ? ;
println! ( ) ;
2026-04-01 03:48:50 +00:00
if let Some ( event ) = summary . auto_compaction {
println! (
" {} " ,
format_auto_compaction_notice ( event . removed_message_count )
) ;
}
2026-03-31 22:49:50 +00:00
self . persist_session ( ) ? ;
2026-03-31 18:39:39 +00:00
Ok ( ( ) )
}
Err ( error ) = > {
2026-04-02 10:04:54 +00:00
runtime . shutdown_plugins ( ) ? ;
2026-03-31 18:39:39 +00:00
spinner . fail (
2026-04-01 01:44:55 +00:00
" ❌ Request failed " ,
2026-03-31 18:39:39 +00:00
TerminalRenderer ::new ( ) . color_theme ( ) ,
& mut stdout ,
) ? ;
Err ( Box ::new ( error ) )
}
}
}
2026-03-31 22:49:50 +00:00
fn run_turn_with_output (
& mut self ,
input : & str ,
output_format : CliOutputFormat ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
match output_format {
CliOutputFormat ::Text = > self . run_turn ( input ) ,
CliOutputFormat ::Json = > self . run_prompt_json ( input ) ,
}
}
fn run_prompt_json ( & mut self , input : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-01 05:58:00 +00:00
let ( mut runtime , hook_abort_monitor ) = self . prepare_turn_runtime ( false ) ? ;
2026-04-01 02:42:49 +00:00
let mut permission_prompter = CliPermissionPrompter ::new ( self . permission_mode ) ;
2026-04-01 05:55:24 +00:00
let result = runtime . run_turn ( input , Some ( & mut permission_prompter ) ) ;
hook_abort_monitor . stop ( ) ;
let summary = result ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-04-01 02:42:49 +00:00
self . persist_session ( ) ? ;
2026-03-31 22:49:50 +00:00
println! (
" {} " ,
json! ( {
2026-04-01 02:42:49 +00:00
" message " : final_assistant_text ( & summary ) ,
2026-03-31 22:49:50 +00:00
" model " : self . model ,
2026-04-01 02:42:49 +00:00
" iterations " : summary . iterations ,
2026-04-01 03:48:50 +00:00
" auto_compaction " : summary . auto_compaction . map ( | event | json! ( {
" removed_messages " : event . removed_message_count ,
" notice " : format_auto_compaction_notice ( event . removed_message_count ) ,
} ) ) ,
2026-04-01 02:42:49 +00:00
" tool_uses " : collect_tool_uses ( & summary ) ,
" tool_results " : collect_tool_results ( & summary ) ,
2026-04-01 06:15:13 +00:00
" prompt_cache_events " : collect_prompt_cache_events ( & summary ) ,
2026-03-31 22:49:50 +00:00
" usage " : {
2026-04-01 02:42:49 +00:00
" input_tokens " : summary . usage . input_tokens ,
" output_tokens " : summary . usage . output_tokens ,
" cache_creation_input_tokens " : summary . usage . cache_creation_input_tokens ,
" cache_read_input_tokens " : summary . usage . cache_read_input_tokens ,
2026-04-03 22:41:42 +09:00
} ,
" estimated_cost " : format_usd (
summary . usage . estimate_cost_usd_with_pricing (
pricing_for_model ( & self . model )
. unwrap_or_else ( runtime ::ModelPricing ::default_sonnet_tier )
) . total_cost_usd ( )
)
2026-03-31 22:49:50 +00:00
} )
) ;
Ok ( ( ) )
}
2026-04-03 05:12:51 +09:00
#[ allow(clippy::too_many_lines) ]
2026-03-31 19:23:05 +00:00
fn handle_repl_command (
& mut self ,
command : SlashCommand ,
2026-04-01 00:14:38 +00:00
) -> Result < bool , Box < dyn std ::error ::Error > > {
Ok ( match command {
SlashCommand ::Help = > {
println! ( " {} " , render_repl_help ( ) ) ;
false
}
SlashCommand ::Status = > {
self . print_status ( ) ;
false
}
2026-04-01 03:55:00 +00:00
SlashCommand ::Bughunter { scope } = > {
self . run_bughunter ( scope . as_deref ( ) ) ? ;
false
}
2026-04-02 18:14:09 +09:00
SlashCommand ::Commit = > {
self . run_commit ( None ) ? ;
2026-04-02 18:10:32 +09:00
false
2026-04-01 03:55:00 +00:00
}
SlashCommand ::Pr { context } = > {
self . run_pr ( context . as_deref ( ) ) ? ;
false
}
SlashCommand ::Issue { context } = > {
self . run_issue ( context . as_deref ( ) ) ? ;
false
}
SlashCommand ::Ultraplan { task } = > {
self . run_ultraplan ( task . as_deref ( ) ) ? ;
false
}
SlashCommand ::Teleport { target } = > {
feat: ultraclaw session outputs — registry tests, MCP bridge, PARITY.md, cleanup
Ultraclaw mode results from 10 parallel opencode sessions:
- PARITY.md: Updated both copies with all 9 landed lanes, commit hashes,
line counts, and test counts. All checklist items marked complete.
- MCP bridge: McpToolRegistry.call_tool now wired to real McpServerManager
via async JSON-RPC (discover_tools -> tools/call -> shutdown)
- Registry tests: Added coverage for TaskRegistry, TeamRegistry,
CronRegistry, PermissionEnforcer, LspRegistry (branch-focused tests)
- Permissions refactor: Simplified authorize_with_context, extracted helpers,
added characterization tests (185 runtime tests pass)
- AI slop cleanup: Removed redundant comments, unused_self suppressions,
tightened unreachable branches
- CLI fixes: Minor adjustments in main.rs and hooks.rs
All 363+ tests pass. Workspace compiles clean.
2026-04-03 18:23:03 +09:00
Self ::run_teleport ( target . as_deref ( ) ) ? ;
2026-04-01 03:55:00 +00:00
false
}
2026-04-02 18:14:09 +09:00
SlashCommand ::DebugToolCall = > {
self . run_debug_tool_call ( None ) ? ;
2026-04-01 03:55:00 +00:00
false
}
2026-04-01 01:14:38 +00:00
SlashCommand ::Sandbox = > {
Self ::print_sandbox_status ( ) ;
false
}
2026-04-01 00:14:38 +00:00
SlashCommand ::Compact = > {
self . compact ( ) ? ;
false
}
2026-03-31 19:23:05 +00:00
SlashCommand ::Model { model } = > self . set_model ( model ) ? ,
2026-03-31 19:27:31 +00:00
SlashCommand ::Permissions { mode } = > self . set_permissions ( mode ) ? ,
2026-03-31 20:42:50 +00:00
SlashCommand ::Clear { confirm } = > self . clear_session ( confirm ) ? ,
2026-04-01 00:14:38 +00:00
SlashCommand ::Cost = > {
self . print_cost ( ) ;
false
}
2026-03-31 19:45:25 +00:00
SlashCommand ::Resume { session_path } = > self . resume_session ( session_path ) ? ,
2026-04-01 00:14:38 +00:00
SlashCommand ::Config { section } = > {
Self ::print_config ( section . as_deref ( ) ) ? ;
false
}
2026-04-02 10:04:40 +00:00
SlashCommand ::Mcp { action , target } = > {
let args = match ( action . as_deref ( ) , target . as_deref ( ) ) {
( None , None ) = > None ,
( Some ( action ) , None ) = > Some ( action . to_string ( ) ) ,
( Some ( action ) , Some ( target ) ) = > Some ( format! ( " {action} {target} " ) ) ,
( None , Some ( target ) ) = > Some ( target . to_string ( ) ) ,
} ;
Self ::print_mcp ( args . as_deref ( ) ) ? ;
false
}
2026-04-01 00:14:38 +00:00
SlashCommand ::Memory = > {
Self ::print_memory ( ) ? ;
false
}
SlashCommand ::Init = > {
2026-04-01 01:14:44 +00:00
run_init ( ) ? ;
2026-04-01 00:14:38 +00:00
false
}
SlashCommand ::Diff = > {
Self ::print_diff ( ) ? ;
false
}
SlashCommand ::Version = > {
Self ::print_version ( ) ;
false
}
SlashCommand ::Export { path } = > {
self . export_session ( path . as_deref ( ) ) ? ;
false
}
2026-03-31 22:49:50 +00:00
SlashCommand ::Session { action , target } = > {
2026-04-01 00:14:38 +00:00
self . handle_session_command ( action . as_deref ( ) , target . as_deref ( ) ) ?
2026-03-31 22:49:50 +00:00
}
2026-04-01 04:30:28 +00:00
SlashCommand ::Plugins { action , target } = > {
self . handle_plugins_command ( action . as_deref ( ) , target . as_deref ( ) ) ?
}
2026-04-01 08:19:25 +00:00
SlashCommand ::Agents { args } = > {
Self ::print_agents ( args . as_deref ( ) ) ? ;
false
}
SlashCommand ::Skills { args } = > {
Self ::print_skills ( args . as_deref ( ) ) ? ;
2026-04-01 18:57:50 +09:00
false
}
feat: add 40 slash commands — command surface 67/141
Port 40 missing user-facing slash commands from upstream parity audit:
Session: /doctor, /login, /logout, /usage, /stats, /rename, /privacy-settings
Workspace: /branch, /add-dir, /files, /hooks, /release-notes
Discovery: /context, /tasks, /doctor, /ide, /desktop
Analysis: /review, /security-review, /advisor, /insights
Appearance: /theme, /vim, /voice, /color, /effort, /fast, /brief,
/output-style, /keybindings, /stickers
Communication: /copy, /share, /feedback, /summary, /tag, /thinkback,
/plan, /exit, /upgrade, /rewind
All commands have full SlashCommandSpec, enum variant, parse arm,
and stub handler. Category system expanded with two new categories.
Tests updated for new counts (67 specs, 39 resume-supported).
fmt/clippy/tests all green.
2026-04-03 08:09:14 +09:00
SlashCommand ::Doctor
| SlashCommand ::Login
| SlashCommand ::Logout
| SlashCommand ::Vim
| SlashCommand ::Upgrade
| SlashCommand ::Stats
| SlashCommand ::Share
| SlashCommand ::Feedback
| SlashCommand ::Files
| SlashCommand ::Fast
| SlashCommand ::Exit
| SlashCommand ::Summary
| SlashCommand ::Desktop
| SlashCommand ::Brief
| SlashCommand ::Advisor
| SlashCommand ::Stickers
| SlashCommand ::Insights
| SlashCommand ::Thinkback
| SlashCommand ::ReleaseNotes
| SlashCommand ::SecurityReview
| SlashCommand ::Keybindings
| SlashCommand ::PrivacySettings
| SlashCommand ::Plan { .. }
| SlashCommand ::Review { .. }
| SlashCommand ::Tasks { .. }
| SlashCommand ::Theme { .. }
| SlashCommand ::Voice { .. }
| SlashCommand ::Usage { .. }
| SlashCommand ::Rename { .. }
| SlashCommand ::Copy { .. }
| SlashCommand ::Hooks { .. }
| SlashCommand ::Context { .. }
| SlashCommand ::Color { .. }
| SlashCommand ::Effort { .. }
| SlashCommand ::Branch { .. }
| SlashCommand ::Rewind { .. }
| SlashCommand ::Ide { .. }
| SlashCommand ::Tag { .. }
| SlashCommand ::OutputStyle { .. }
| SlashCommand ::AddDir { .. } = > {
eprintln! ( " Command registered but not yet implemented. " ) ;
false
}
2026-04-01 00:14:38 +00:00
SlashCommand ::Unknown ( name ) = > {
2026-04-02 07:15:03 +00:00
eprintln! ( " {} " , format_unknown_slash_command ( & name ) ) ;
2026-04-01 00:14:38 +00:00
false
}
} )
2026-03-31 19:23:05 +00:00
}
2026-03-31 22:49:50 +00:00
fn persist_session ( & self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
self . runtime . session ( ) . save_to_path ( & self . session . path ) ? ;
Ok ( ( ) )
}
2026-03-31 18:39:39 +00:00
fn print_status ( & self ) {
2026-03-31 19:23:05 +00:00
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
let latest = self . runtime . usage ( ) . current_turn_usage ( ) ;
2026-03-31 18:39:39 +00:00
println! (
2026-03-31 19:23:05 +00:00
" {} " ,
2026-03-31 20:22:59 +00:00
format_status_report (
2026-03-31 19:23:05 +00:00
& self . model ,
2026-03-31 20:22:59 +00:00
StatusUsage {
message_count : self . runtime . session ( ) . messages . len ( ) ,
turns : self . runtime . usage ( ) . turns ( ) ,
latest ,
cumulative ,
estimated_tokens : self . runtime . estimated_tokens ( ) ,
} ,
2026-04-01 00:06:15 +00:00
self . permission_mode . as_str ( ) ,
2026-03-31 22:49:50 +00:00
& status_context ( Some ( & self . session . path ) ) . expect ( " status context should load " ) ,
2026-03-31 19:23:05 +00:00
)
2026-03-31 18:39:39 +00:00
) ;
}
2026-04-01 01:14:38 +00:00
fn print_sandbox_status ( ) {
let cwd = env ::current_dir ( ) . expect ( " current dir " ) ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let runtime_config = loader
. load ( )
. unwrap_or_else ( | _ | runtime ::RuntimeConfig ::empty ( ) ) ;
println! (
" {} " ,
format_sandbox_report ( & resolve_sandbox_status ( runtime_config . sandbox ( ) , & cwd ) )
) ;
}
2026-04-01 00:14:38 +00:00
fn set_model ( & mut self , model : Option < String > ) -> Result < bool , Box < dyn std ::error ::Error > > {
2026-03-31 19:23:05 +00:00
let Some ( model ) = model else {
2026-03-31 20:43:56 +00:00
println! (
" {} " ,
format_model_report (
& self . model ,
self . runtime . session ( ) . messages . len ( ) ,
self . runtime . usage ( ) . turns ( ) ,
)
) ;
2026-04-01 00:14:38 +00:00
return Ok ( false ) ;
2026-03-31 19:23:05 +00:00
} ;
2026-04-01 02:04:12 +00:00
let model = resolve_model_alias ( & model ) . to_string ( ) ;
2026-03-31 19:23:05 +00:00
if model = = self . model {
2026-03-31 20:43:56 +00:00
println! (
" {} " ,
format_model_report (
& self . model ,
self . runtime . session ( ) . messages . len ( ) ,
self . runtime . usage ( ) . turns ( ) ,
)
) ;
2026-04-01 00:14:38 +00:00
return Ok ( false ) ;
2026-03-31 19:23:05 +00:00
}
2026-03-31 20:43:56 +00:00
let previous = self . model . clone ( ) ;
2026-03-31 19:23:05 +00:00
let session = self . runtime . session ( ) . clone ( ) ;
2026-03-31 20:43:56 +00:00
let message_count = session . messages . len ( ) ;
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-03-31 23:38:53 +00:00
session ,
2026-04-01 04:40:21 +00:00
& self . session . id ,
2026-03-31 23:38:53 +00:00
model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-04-01 00:06:15 +00:00
self . permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-03-31 23:38:53 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-03-31 19:23:05 +00:00
self . model . clone_from ( & model ) ;
2026-03-31 20:43:56 +00:00
println! (
" {} " ,
format_model_switch_report ( & previous , & model , message_count )
) ;
2026-04-01 00:14:38 +00:00
Ok ( true )
2026-03-31 19:23:05 +00:00
}
2026-04-01 00:14:38 +00:00
fn set_permissions (
& mut self ,
mode : Option < String > ,
) -> Result < bool , Box < dyn std ::error ::Error > > {
2026-03-31 19:27:31 +00:00
let Some ( mode ) = mode else {
2026-04-01 00:06:15 +00:00
println! (
" {} " ,
format_permissions_report ( self . permission_mode . as_str ( ) )
) ;
2026-04-01 00:14:38 +00:00
return Ok ( false ) ;
2026-03-31 19:27:31 +00:00
} ;
let normalized = normalize_permission_mode ( & mode ) . ok_or_else ( | | {
format! (
2026-03-31 22:49:50 +00:00
" unsupported permission mode '{mode}'. Use read-only, workspace-write, or danger-full-access. "
2026-03-31 19:27:31 +00:00
)
} ) ? ;
2026-04-01 00:06:15 +00:00
if normalized = = self . permission_mode . as_str ( ) {
2026-03-31 21:01:21 +00:00
println! ( " {} " , format_permissions_report ( normalized ) ) ;
2026-04-01 00:14:38 +00:00
return Ok ( false ) ;
2026-03-31 19:27:31 +00:00
}
2026-04-01 00:06:15 +00:00
let previous = self . permission_mode . as_str ( ) . to_string ( ) ;
2026-03-31 19:27:31 +00:00
let session = self . runtime . session ( ) . clone ( ) ;
2026-04-01 00:06:15 +00:00
self . permission_mode = permission_mode_from_label ( normalized ) ;
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-03-31 19:27:31 +00:00
session ,
2026-04-01 04:30:29 +00:00
& self . session . id ,
2026-03-31 19:27:31 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-04-01 00:06:15 +00:00
self . permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-03-31 19:27:31 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-03-31 21:01:21 +00:00
println! (
" {} " ,
format_permissions_switch_report ( & previous , normalized )
) ;
2026-04-01 00:14:38 +00:00
Ok ( true )
2026-03-31 19:27:31 +00:00
}
2026-04-01 00:14:38 +00:00
fn clear_session ( & mut self , confirm : bool ) -> Result < bool , Box < dyn std ::error ::Error > > {
2026-03-31 20:42:50 +00:00
if ! confirm {
println! (
" clear: confirmation required; run /clear --confirm to start a fresh session. "
) ;
2026-04-01 00:14:38 +00:00
return Ok ( false ) ;
2026-03-31 20:42:50 +00:00
}
2026-04-02 10:03:07 +00:00
let previous_session = self . session . clone ( ) ;
2026-04-01 04:30:27 +00:00
let session_state = Session ::new ( ) ;
self . session = create_managed_session_handle ( & session_state . session_id ) ? ;
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-04-01 04:30:27 +00:00
session_state . with_persistence_path ( self . session . path . clone ( ) ) ,
2026-04-02 11:16:56 +09:00
& self . session . id ,
2026-03-31 19:27:31 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-04-01 00:06:15 +00:00
self . permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-03-31 19:27:31 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-03-31 21:03:49 +00:00
println! (
2026-04-02 10:03:07 +00:00
" Session cleared \n Mode fresh session \n Previous session {} \n Resume previous /resume {} \n Preserved model {} \n Permission mode {} \n New session {} \n Session file {} " ,
previous_session . id ,
previous_session . id ,
2026-03-31 21:03:49 +00:00
self . model ,
2026-04-01 00:06:15 +00:00
self . permission_mode . as_str ( ) ,
2026-03-31 22:49:50 +00:00
self . session . id ,
2026-04-02 10:03:07 +00:00
self . session . path . display ( ) ,
2026-03-31 21:03:49 +00:00
) ;
2026-04-01 00:14:38 +00:00
Ok ( true )
2026-03-31 19:27:31 +00:00
}
fn print_cost ( & self ) {
let cumulative = self . runtime . usage ( ) . cumulative_usage ( ) ;
2026-03-31 21:02:24 +00:00
println! ( " {} " , format_cost_report ( cumulative ) ) ;
2026-03-31 19:27:31 +00:00
}
2026-03-31 19:45:25 +00:00
fn resume_session (
& mut self ,
session_path : Option < String > ,
2026-04-01 00:14:38 +00:00
) -> Result < bool , Box < dyn std ::error ::Error > > {
2026-03-31 22:49:50 +00:00
let Some ( session_ref ) = session_path else {
2026-04-02 07:15:03 +00:00
println! ( " {} " , render_resume_usage ( ) ) ;
2026-04-01 00:14:38 +00:00
return Ok ( false ) ;
2026-03-31 19:45:25 +00:00
} ;
2026-03-31 22:49:50 +00:00
let handle = resolve_session_reference ( & session_ref ) ? ;
let session = Session ::load_from_path ( & handle . path ) ? ;
2026-03-31 19:45:25 +00:00
let message_count = session . messages . len ( ) ;
2026-04-01 04:30:27 +00:00
let session_id = session . session_id . clone ( ) ;
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-03-31 19:45:25 +00:00
session ,
2026-04-01 04:40:21 +00:00
& handle . id ,
2026-03-31 19:45:25 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-04-01 00:06:15 +00:00
self . permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-03-31 19:45:25 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-04-01 04:30:27 +00:00
self . session = SessionHandle {
id : session_id ,
path : handle . path ,
} ;
2026-03-31 21:04:42 +00:00
println! (
" {} " ,
2026-03-31 22:49:50 +00:00
format_resume_report (
& self . session . path . display ( ) . to_string ( ) ,
message_count ,
self . runtime . usage ( ) . turns ( ) ,
)
2026-03-31 21:04:42 +00:00
) ;
2026-04-01 00:14:38 +00:00
Ok ( true )
2026-03-31 19:45:25 +00:00
}
2026-03-31 21:11:57 +00:00
fn print_config ( section : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , render_config_report ( section ) ? ) ;
2026-03-31 19:54:09 +00:00
Ok ( ( ) )
}
2026-03-31 19:45:25 +00:00
2026-03-31 19:54:09 +00:00
fn print_memory ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , render_memory_report ( ) ? ) ;
2026-03-31 19:45:25 +00:00
Ok ( ( ) )
}
2026-04-01 08:19:25 +00:00
fn print_agents ( args : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
println! ( " {} " , handle_agents_slash_command ( args , & cwd ) ? ) ;
Ok ( ( ) )
}
2026-04-02 10:04:40 +00:00
fn print_mcp ( args : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
println! ( " {} " , handle_mcp_slash_command ( args , & cwd ) ? ) ;
Ok ( ( ) )
}
2026-04-01 08:19:25 +00:00
fn print_skills ( args : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
println! ( " {} " , handle_skills_slash_command ( args , & cwd ) ? ) ;
Ok ( ( ) )
}
2026-03-31 22:49:50 +00:00
fn print_diff ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , render_diff_report ( ) ? ) ;
Ok ( ( ) )
}
fn print_version ( ) {
println! ( " {} " , render_version_report ( ) ) ;
}
fn export_session (
& self ,
requested_path : Option < & str > ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let export_path = resolve_export_path ( requested_path , self . runtime . session ( ) ) ? ;
fs ::write ( & export_path , render_export_text ( self . runtime . session ( ) ) ) ? ;
println! (
" Export \n Result wrote transcript \n File {} \n Messages {} " ,
export_path . display ( ) ,
self . runtime . session ( ) . messages . len ( ) ,
) ;
Ok ( ( ) )
}
fn handle_session_command (
& mut self ,
action : Option < & str > ,
target : Option < & str > ,
2026-04-01 00:14:38 +00:00
) -> Result < bool , Box < dyn std ::error ::Error > > {
2026-03-31 22:49:50 +00:00
match action {
None | Some ( " list " ) = > {
println! ( " {} " , render_session_list ( & self . session . id ) ? ) ;
2026-04-01 00:14:38 +00:00
Ok ( false )
2026-03-31 22:49:50 +00:00
}
Some ( " switch " ) = > {
let Some ( target ) = target else {
println! ( " Usage: /session switch <session-id> " ) ;
2026-04-01 00:14:38 +00:00
return Ok ( false ) ;
2026-03-31 22:49:50 +00:00
} ;
let handle = resolve_session_reference ( target ) ? ;
let session = Session ::load_from_path ( & handle . path ) ? ;
let message_count = session . messages . len ( ) ;
2026-04-01 04:30:27 +00:00
let session_id = session . session_id . clone ( ) ;
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-03-31 22:49:50 +00:00
session ,
2026-04-01 04:30:29 +00:00
& handle . id ,
2026-03-31 22:49:50 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-04-01 00:06:15 +00:00
self . permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-03-31 22:49:50 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-04-01 04:30:27 +00:00
self . session = SessionHandle {
id : session_id ,
path : handle . path ,
} ;
2026-03-31 22:49:50 +00:00
println! (
" Session switched \n Active session {} \n File {} \n Messages {} " ,
self . session . id ,
self . session . path . display ( ) ,
message_count ,
) ;
2026-04-01 00:14:38 +00:00
Ok ( true )
2026-03-31 22:49:50 +00:00
}
2026-04-01 06:15:14 +00:00
Some ( " fork " ) = > {
let forked = self . runtime . fork_session ( target . map ( ToOwned ::to_owned ) ) ;
let parent_session_id = self . session . id . clone ( ) ;
let handle = create_managed_session_handle ( & forked . session_id ) ? ;
let branch_name = forked
. fork
. as_ref ( )
. and_then ( | fork | fork . branch_name . clone ( ) ) ;
let forked = forked . with_persistence_path ( handle . path . clone ( ) ) ;
let message_count = forked . messages . len ( ) ;
forked . save_to_path ( & handle . path ) ? ;
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-04-01 06:15:14 +00:00
forked ,
2026-04-02 11:16:56 +09:00
& handle . id ,
2026-04-01 06:15:14 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
true ,
self . allowed_tools . clone ( ) ,
self . permission_mode ,
2026-04-02 11:10:48 +09:00
None ,
2026-04-01 06:15:14 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-04-01 06:15:14 +00:00
self . session = handle ;
println! (
" Session forked \n Parent session {} \n Active session {} \n Branch {} \n File {} \n Messages {} " ,
parent_session_id ,
self . session . id ,
branch_name . as_deref ( ) . unwrap_or ( " (unnamed) " ) ,
self . session . path . display ( ) ,
message_count ,
) ;
Ok ( true )
}
2026-03-31 22:49:50 +00:00
Some ( other ) = > {
2026-04-01 06:15:14 +00:00
println! (
" Unknown /session action '{other}'. Use /session list, /session switch <session-id>, or /session fork [branch-name]. "
) ;
2026-04-01 00:14:38 +00:00
Ok ( false )
2026-03-31 22:49:50 +00:00
}
}
}
2026-04-01 04:30:28 +00:00
fn handle_plugins_command (
& mut self ,
action : Option < & str > ,
target : Option < & str > ,
) -> Result < bool , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
2026-04-01 04:40:19 +00:00
let loader = ConfigLoader ::default_for ( & cwd ) ;
let runtime_config = loader . load ( ) ? ;
let mut manager = build_plugin_manager ( & cwd , & loader , & runtime_config ) ;
2026-04-01 06:45:13 +00:00
let result = handle_plugins_slash_command ( action , target , & mut manager ) ? ;
println! ( " {} " , result . message ) ;
if result . reload_runtime {
self . reload_runtime_features ( ) ? ;
2026-04-01 04:30:28 +00:00
}
Ok ( false )
}
fn reload_runtime_features ( & mut self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-04-01 04:30:28 +00:00
self . runtime . session ( ) . clone ( ) ,
2026-04-02 11:16:56 +09:00
& self . session . id ,
2026-04-01 04:30:28 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
true ,
self . allowed_tools . clone ( ) ,
self . permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-04-01 04:30:28 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-04-01 04:30:28 +00:00
self . persist_session ( )
}
2026-03-31 18:39:39 +00:00
fn compact ( & mut self ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let result = self . runtime . compact ( CompactionConfig ::default ( ) ) ;
let removed = result . removed_message_count ;
2026-03-31 21:15:37 +00:00
let kept = result . compacted_session . messages . len ( ) ;
let skipped = removed = = 0 ;
2026-04-02 10:04:54 +00:00
let runtime = build_runtime (
2026-03-31 18:39:39 +00:00
result . compacted_session ,
2026-04-01 04:30:29 +00:00
& self . session . id ,
2026-03-31 18:39:39 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
true ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 23:38:53 +00:00
self . allowed_tools . clone ( ) ,
2026-04-01 00:06:15 +00:00
self . permission_mode ,
2026-04-01 08:05:22 +00:00
None ,
2026-03-31 18:39:39 +00:00
) ? ;
2026-04-02 10:04:54 +00:00
self . replace_runtime ( runtime ) ? ;
2026-03-31 22:49:50 +00:00
self . persist_session ( ) ? ;
2026-03-31 21:15:37 +00:00
println! ( " {} " , format_compact_report ( removed , kept , skipped ) ) ;
2026-03-31 18:39:39 +00:00
Ok ( ( ) )
}
2026-04-01 03:55:00 +00:00
2026-04-01 08:05:22 +00:00
fn run_internal_prompt_text_with_progress (
2026-04-01 03:55:00 +00:00
& self ,
prompt : & str ,
enable_tools : bool ,
2026-04-01 08:05:22 +00:00
progress : Option < InternalPromptProgressReporter > ,
2026-04-01 03:55:00 +00:00
) -> Result < String , Box < dyn std ::error ::Error > > {
let session = self . runtime . session ( ) . clone ( ) ;
let mut runtime = build_runtime (
session ,
2026-04-02 11:16:56 +09:00
& self . session . id ,
2026-04-01 03:55:00 +00:00
self . model . clone ( ) ,
self . system_prompt . clone ( ) ,
enable_tools ,
false ,
self . allowed_tools . clone ( ) ,
self . permission_mode ,
2026-04-01 08:05:22 +00:00
progress ,
2026-04-01 03:55:00 +00:00
) ? ;
let mut permission_prompter = CliPermissionPrompter ::new ( self . permission_mode ) ;
let summary = runtime . run_turn ( prompt , Some ( & mut permission_prompter ) ) ? ;
2026-04-02 10:04:54 +00:00
let text = final_assistant_text ( & summary ) . trim ( ) . to_string ( ) ;
runtime . shutdown_plugins ( ) ? ;
Ok ( text )
2026-04-01 03:55:00 +00:00
}
2026-04-01 08:05:22 +00:00
fn run_internal_prompt_text (
& self ,
prompt : & str ,
enable_tools : bool ,
) -> Result < String , Box < dyn std ::error ::Error > > {
self . run_internal_prompt_text_with_progress ( prompt , enable_tools , None )
}
2026-04-01 03:55:00 +00:00
fn run_bughunter ( & self , scope : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-02 18:10:32 +09:00
println! ( " {} " , format_bughunter_report ( scope ) ) ;
2026-04-01 03:55:00 +00:00
Ok ( ( ) )
}
fn run_ultraplan ( & self , task : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-02 18:10:32 +09:00
println! ( " {} " , format_ultraplan_report ( task ) ) ;
Ok ( ( ) )
2026-04-01 03:55:00 +00:00
}
feat: ultraclaw session outputs — registry tests, MCP bridge, PARITY.md, cleanup
Ultraclaw mode results from 10 parallel opencode sessions:
- PARITY.md: Updated both copies with all 9 landed lanes, commit hashes,
line counts, and test counts. All checklist items marked complete.
- MCP bridge: McpToolRegistry.call_tool now wired to real McpServerManager
via async JSON-RPC (discover_tools -> tools/call -> shutdown)
- Registry tests: Added coverage for TaskRegistry, TeamRegistry,
CronRegistry, PermissionEnforcer, LspRegistry (branch-focused tests)
- Permissions refactor: Simplified authorize_with_context, extracted helpers,
added characterization tests (185 runtime tests pass)
- AI slop cleanup: Removed redundant comments, unused_self suppressions,
tightened unreachable branches
- CLI fixes: Minor adjustments in main.rs and hooks.rs
All 363+ tests pass. Workspace compiles clean.
2026-04-03 18:23:03 +09:00
fn run_teleport ( target : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-01 03:55:00 +00:00
let Some ( target ) = target . map ( str ::trim ) . filter ( | value | ! value . is_empty ( ) ) else {
println! ( " Usage: /teleport <symbol-or-path> " ) ;
return Ok ( ( ) ) ;
} ;
println! ( " {} " , render_teleport_report ( target ) ? ) ;
Ok ( ( ) )
}
2026-04-02 18:14:09 +09:00
fn run_debug_tool_call ( & self , args : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-02 18:10:32 +09:00
validate_no_args ( " /debug-tool-call " , args ) ? ;
2026-04-01 03:55:00 +00:00
println! ( " {} " , render_last_tool_debug_report ( self . runtime . session ( ) ) ? ) ;
Ok ( ( ) )
}
2026-04-02 18:10:32 +09:00
fn run_commit ( & mut self , args : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
validate_no_args ( " /commit " , args ) ? ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
let status = git_output ( & [ " status " , " --short " , " --branch " ] ) ? ;
let summary = parse_git_workspace_summary ( Some ( & status ) ) ;
let branch = parse_git_status_branch ( Some ( & status ) ) ;
if summary . is_clean ( ) {
println! ( " {} " , format_commit_skipped_report ( ) ) ;
2026-04-01 03:55:00 +00:00
return Ok ( ( ) ) ;
}
2026-04-02 18:14:09 +09:00
println! (
" {} " ,
format_commit_preflight_report ( branch . as_deref ( ) , summary )
) ;
2026-04-01 03:55:00 +00:00
Ok ( ( ) )
}
fn run_pr ( & self , context : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-02 18:14:09 +09:00
let branch =
resolve_git_branch_for ( & env ::current_dir ( ) ? ) . unwrap_or_else ( | | " unknown " . to_string ( ) ) ;
2026-04-02 18:10:32 +09:00
println! ( " {} " , format_pr_report ( & branch , context ) ) ;
2026-04-01 03:55:00 +00:00
Ok ( ( ) )
}
fn run_issue ( & self , context : Option < & str > ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-02 18:10:32 +09:00
println! ( " {} " , format_issue_report ( context ) ) ;
2026-04-01 03:55:00 +00:00
Ok ( ( ) )
}
2026-03-31 18:39:39 +00:00
}
2026-03-31 22:49:50 +00:00
fn sessions_dir ( ) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
2026-04-01 18:57:50 +09:00
let path = cwd . join ( " .claw " ) . join ( " sessions " ) ;
2026-03-31 22:49:50 +00:00
fs ::create_dir_all ( & path ) ? ;
Ok ( path )
}
2026-04-01 04:30:27 +00:00
fn create_managed_session_handle (
session_id : & str ,
) -> Result < SessionHandle , Box < dyn std ::error ::Error > > {
let id = session_id . to_string ( ) ;
2026-04-01 06:15:14 +00:00
let path = sessions_dir ( ) ? . join ( format! ( " {id} . {PRIMARY_SESSION_EXTENSION} " ) ) ;
2026-03-31 22:49:50 +00:00
Ok ( SessionHandle { id , path } )
}
fn resolve_session_reference ( reference : & str ) -> Result < SessionHandle , Box < dyn std ::error ::Error > > {
2026-04-02 07:15:03 +00:00
if SESSION_REFERENCE_ALIASES
. iter ( )
. any ( | alias | reference . eq_ignore_ascii_case ( alias ) )
{
let latest = latest_managed_session ( ) ? ;
return Ok ( SessionHandle {
id : latest . id ,
path : latest . path ,
} ) ;
}
2026-03-31 22:49:50 +00:00
let direct = PathBuf ::from ( reference ) ;
2026-04-01 06:15:14 +00:00
let looks_like_path = direct . extension ( ) . is_some ( ) | | direct . components ( ) . count ( ) > 1 ;
2026-03-31 22:49:50 +00:00
let path = if direct . exists ( ) {
direct
2026-04-01 06:15:14 +00:00
} else if looks_like_path {
2026-04-02 07:15:03 +00:00
return Err ( format_missing_session_reference ( reference ) . into ( ) ) ;
2026-03-31 22:49:50 +00:00
} else {
2026-04-01 06:15:14 +00:00
resolve_managed_session_path ( reference ) ?
2026-03-31 22:49:50 +00:00
} ;
let id = path
2026-04-01 06:15:14 +00:00
. file_name ( )
2026-03-31 22:49:50 +00:00
. and_then ( | value | value . to_str ( ) )
2026-04-01 06:15:14 +00:00
. and_then ( | name | {
name . strip_suffix ( & format! ( " . {PRIMARY_SESSION_EXTENSION} " ) )
. or_else ( | | name . strip_suffix ( & format! ( " . {LEGACY_SESSION_EXTENSION} " ) ) )
} )
2026-03-31 22:49:50 +00:00
. unwrap_or ( reference )
. to_string ( ) ;
Ok ( SessionHandle { id , path } )
}
2026-04-01 06:15:14 +00:00
fn resolve_managed_session_path ( session_id : & str ) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let directory = sessions_dir ( ) ? ;
for extension in [ PRIMARY_SESSION_EXTENSION , LEGACY_SESSION_EXTENSION ] {
let path = directory . join ( format! ( " {session_id} . {extension} " ) ) ;
if path . exists ( ) {
return Ok ( path ) ;
}
}
2026-04-02 07:15:03 +00:00
Err ( format_missing_session_reference ( session_id ) . into ( ) )
2026-04-01 06:15:14 +00:00
}
fn is_managed_session_file ( path : & Path ) -> bool {
path . extension ( )
. and_then ( | ext | ext . to_str ( ) )
. is_some_and ( | extension | {
extension = = PRIMARY_SESSION_EXTENSION | | extension = = LEGACY_SESSION_EXTENSION
} )
}
2026-03-31 22:49:50 +00:00
fn list_managed_sessions ( ) -> Result < Vec < ManagedSessionSummary > , Box < dyn std ::error ::Error > > {
let mut sessions = Vec ::new ( ) ;
for entry in fs ::read_dir ( sessions_dir ( ) ? ) ? {
let entry = entry ? ;
let path = entry . path ( ) ;
2026-04-01 06:15:14 +00:00
if ! is_managed_session_file ( & path ) {
2026-03-31 22:49:50 +00:00
continue ;
}
let metadata = entry . metadata ( ) ? ;
2026-04-02 07:15:03 +00:00
let modified_epoch_millis = metadata
2026-03-31 22:49:50 +00:00
. modified ( )
. ok ( )
. and_then ( | time | time . duration_since ( UNIX_EPOCH ) . ok ( ) )
2026-04-02 07:15:03 +00:00
. map ( | duration | duration . as_millis ( ) )
2026-03-31 22:49:50 +00:00
. unwrap_or_default ( ) ;
2026-04-02 07:19:14 +00:00
let ( id , message_count , parent_session_id , branch_name ) =
match Session ::load_from_path ( & path ) {
Ok ( session ) = > {
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
let parent_session_id = session
. fork
. as_ref ( )
. map ( | fork | fork . parent_session_id . clone ( ) ) ;
let branch_name = session
. fork
. as_ref ( )
. and_then ( | fork | fork . branch_name . clone ( ) ) ;
(
session . session_id ,
session . messages . len ( ) ,
parent_session_id ,
branch_name ,
)
2026-04-02 07:19:14 +00:00
}
Err ( _ ) = > (
2026-04-01 04:30:27 +00:00
path . file_stem ( )
. and_then ( | value | value . to_str ( ) )
. unwrap_or ( " unknown " )
. to_string ( ) ,
0 ,
2026-04-01 06:15:14 +00:00
None ,
None ,
2026-04-02 07:19:14 +00:00
) ,
} ;
2026-03-31 22:49:50 +00:00
sessions . push ( ManagedSessionSummary {
id ,
path ,
2026-04-02 07:15:03 +00:00
modified_epoch_millis ,
2026-03-31 22:49:50 +00:00
message_count ,
2026-04-01 06:15:14 +00:00
parent_session_id ,
branch_name ,
2026-03-31 22:49:50 +00:00
} ) ;
}
2026-04-02 07:15:03 +00:00
sessions . sort_by ( | left , right | {
right
. modified_epoch_millis
. cmp ( & left . modified_epoch_millis )
. then_with ( | | right . id . cmp ( & left . id ) )
} ) ;
2026-03-31 22:49:50 +00:00
Ok ( sessions )
}
2026-04-02 07:15:03 +00:00
fn latest_managed_session ( ) -> Result < ManagedSessionSummary , Box < dyn std ::error ::Error > > {
list_managed_sessions ( ) ?
. into_iter ( )
. next ( )
. ok_or_else ( | | format_no_managed_sessions ( ) . into ( ) )
}
fn format_missing_session_reference ( reference : & str ) -> String {
format! (
" session not found: {reference} \n Hint: managed sessions live in .claw/sessions/. Try `{LATEST_SESSION_REFERENCE}` for the most recent session or `/session list` in the REPL. "
)
}
fn format_no_managed_sessions ( ) -> String {
format! (
" no managed sessions found in .claw/sessions/ \n Start `claw` to create a session, then rerun with `--resume {LATEST_SESSION_REFERENCE}`. "
)
}
2026-03-31 22:49:50 +00:00
fn render_session_list ( active_session_id : & str ) -> Result < String , Box < dyn std ::error ::Error > > {
let sessions = list_managed_sessions ( ) ? ;
let mut lines = vec! [
" Sessions " . to_string ( ) ,
format! ( " Directory {} " , sessions_dir ( ) ? . display ( ) ) ,
] ;
if sessions . is_empty ( ) {
lines . push ( " No managed sessions saved yet. " . to_string ( ) ) ;
return Ok ( lines . join ( " \n " ) ) ;
}
for session in sessions {
let marker = if session . id = = active_session_id {
" ● current "
} else {
" ○ saved "
} ;
2026-04-01 06:15:14 +00:00
let lineage = match (
session . branch_name . as_deref ( ) ,
session . parent_session_id . as_deref ( ) ,
) {
( Some ( branch_name ) , Some ( parent_session_id ) ) = > {
format! ( " branch= {branch_name} from= {parent_session_id} " )
}
( None , Some ( parent_session_id ) ) = > format! ( " from= {parent_session_id} " ) ,
( Some ( branch_name ) , None ) = > format! ( " branch= {branch_name} " ) ,
( None , None ) = > String ::new ( ) ,
} ;
2026-03-31 22:49:50 +00:00
lines . push ( format! (
2026-04-01 06:15:14 +00:00
" {id:<20} {marker:<10} msgs={msgs:<4} modified={modified}{lineage} path={path} " ,
2026-03-31 22:49:50 +00:00
id = session . id ,
msgs = session . message_count ,
2026-04-02 07:15:03 +00:00
modified = format_session_modified_age ( session . modified_epoch_millis ) ,
2026-04-01 06:15:14 +00:00
lineage = lineage ,
2026-03-31 22:49:50 +00:00
path = session . path . display ( ) ,
) ) ;
}
Ok ( lines . join ( " \n " ) )
}
2026-04-02 07:15:03 +00:00
fn format_session_modified_age ( modified_epoch_millis : u128 ) -> String {
let now = std ::time ::SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. ok ( )
. map_or ( modified_epoch_millis , | duration | duration . as_millis ( ) ) ;
let delta_seconds = now
. saturating_sub ( modified_epoch_millis )
. checked_div ( 1_000 )
. unwrap_or_default ( ) ;
match delta_seconds {
0 ..= 4 = > " just-now " . to_string ( ) ,
5 ..= 59 = > format! ( " {delta_seconds} s-ago " ) ,
60 ..= 3_599 = > format! ( " {} m-ago " , delta_seconds / 60 ) ,
3_600 ..= 86_399 = > format! ( " {} h-ago " , delta_seconds / 3_600 ) ,
_ = > format! ( " {} d-ago " , delta_seconds / 86_400 ) ,
}
}
2026-04-02 10:03:07 +00:00
fn write_session_clear_backup (
session : & Session ,
session_path : & Path ,
) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let backup_path = session_clear_backup_path ( session_path ) ;
session . save_to_path ( & backup_path ) ? ;
Ok ( backup_path )
}
fn session_clear_backup_path ( session_path : & Path ) -> PathBuf {
let timestamp = std ::time ::SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. ok ( )
. map_or ( 0 , | duration | duration . as_millis ( ) ) ;
let file_name = session_path
. file_name ( )
. and_then ( | value | value . to_str ( ) )
. unwrap_or ( " session.jsonl " ) ;
session_path . with_file_name ( format! ( " {file_name} .before-clear- {timestamp} .bak " ) )
}
2026-03-31 19:23:05 +00:00
fn render_repl_help ( ) -> String {
2026-03-31 21:03:49 +00:00
[
" REPL " . to_string ( ) ,
" /exit Quit the REPL " . to_string ( ) ,
" /quit Quit the REPL " . to_string ( ) ,
2026-04-01 00:14:38 +00:00
" Up/Down Navigate prompt history " . to_string ( ) ,
2026-04-02 07:19:14 +00:00
" Tab Complete commands, modes, and recent sessions " . to_string ( ) ,
2026-04-01 00:14:38 +00:00
" Ctrl-C Clear input (or exit on empty prompt) " . to_string ( ) ,
" Shift+Enter/Ctrl+J Insert a newline " . to_string ( ) ,
2026-04-02 07:15:03 +00:00
" Auto-save .claw/sessions/<session-id>.jsonl " . to_string ( ) ,
" Resume latest /resume latest " . to_string ( ) ,
" Browse sessions /session list " . to_string ( ) ,
2026-03-31 21:03:49 +00:00
String ::new ( ) ,
render_slash_command_help ( ) ,
]
. join (
"
" ,
2026-03-31 19:23:05 +00:00
)
}
2026-04-02 07:44:39 +00:00
fn print_status_snapshot (
model : & str ,
permission_mode : PermissionMode ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! (
" {} " ,
format_status_report (
model ,
StatusUsage {
message_count : 0 ,
turns : 0 ,
latest : TokenUsage ::default ( ) ,
cumulative : TokenUsage ::default ( ) ,
estimated_tokens : 0 ,
} ,
permission_mode . as_str ( ) ,
& status_context ( None ) ? ,
)
) ;
Ok ( ( ) )
}
2026-03-31 20:22:59 +00:00
fn status_context (
session_path : Option < & Path > ,
) -> Result < StatusContext , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let discovered_config_files = loader . discover ( ) . len ( ) ;
let runtime_config = loader . load ( ) ? ;
2026-03-31 21:06:51 +00:00
let project_context = ProjectContext ::discover_with_git ( & cwd , DEFAULT_DATE ) ? ;
let ( project_root , git_branch ) =
parse_git_status_metadata ( project_context . git_status . as_deref ( ) ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
let git_summary = parse_git_workspace_summary ( project_context . git_status . as_deref ( ) ) ;
2026-04-01 01:14:38 +00:00
let sandbox_status = resolve_sandbox_status ( runtime_config . sandbox ( ) , & cwd ) ;
2026-03-31 20:22:59 +00:00
Ok ( StatusContext {
cwd ,
session_path : session_path . map ( Path ::to_path_buf ) ,
loaded_config_files : runtime_config . loaded_entries ( ) . len ( ) ,
discovered_config_files ,
memory_file_count : project_context . instruction_files . len ( ) ,
2026-03-31 21:06:51 +00:00
project_root ,
git_branch ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
git_summary ,
2026-04-01 01:14:38 +00:00
sandbox_status ,
2026-03-31 20:22:59 +00:00
} )
}
fn format_status_report (
2026-03-31 19:23:05 +00:00
model : & str ,
2026-03-31 20:22:59 +00:00
usage : StatusUsage ,
2026-03-31 19:23:05 +00:00
permission_mode : & str ,
2026-03-31 20:22:59 +00:00
context : & StatusContext ,
2026-03-31 19:23:05 +00:00
) -> String {
2026-03-31 20:41:39 +00:00
[
format! (
" Status
Model { model }
Permission mode { permission_mode }
Messages { }
Turns { }
Estimated tokens { } " ,
usage . message_count , usage . turns , usage . estimated_tokens ,
) ,
format! (
" Usage
Latest total { }
Cumulative input { }
Cumulative output { }
Cumulative total { } " ,
usage . latest . total_tokens ( ) ,
usage . cumulative . input_tokens ,
usage . cumulative . output_tokens ,
usage . cumulative . total_tokens ( ) ,
) ,
format! (
" Workspace
Cwd { }
2026-03-31 21:06:51 +00:00
Project root { }
Git branch { }
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
Git state { }
Changed files { }
Staged { }
Unstaged { }
Untracked { }
2026-03-31 20:41:39 +00:00
Session { }
Config files loaded { } / { }
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
Memory files { }
Suggested flow / status → / diff → / commit " ,
2026-03-31 20:41:39 +00:00
context . cwd . display ( ) ,
2026-03-31 21:06:51 +00:00
context
. project_root
. as_ref ( )
. map_or_else ( | | " unknown " . to_string ( ) , | path | path . display ( ) . to_string ( ) ) ,
context . git_branch . as_deref ( ) . unwrap_or ( " unknown " ) ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
context . git_summary . headline ( ) ,
context . git_summary . changed_files ,
context . git_summary . staged_files ,
context . git_summary . unstaged_files ,
context . git_summary . untracked_files ,
2026-03-31 20:41:39 +00:00
context . session_path . as_ref ( ) . map_or_else (
| | " live-repl " . to_string ( ) ,
| path | path . display ( ) . to_string ( )
) ,
context . loaded_config_files ,
context . discovered_config_files ,
context . memory_file_count ,
) ,
2026-04-01 01:14:38 +00:00
format_sandbox_report ( & context . sandbox_status ) ,
2026-03-31 20:41:39 +00:00
]
. join (
2026-03-31 20:22:59 +00:00
"
2026-03-31 20:41:39 +00:00
2026-03-31 20:22:59 +00:00
" ,
2026-03-31 19:23:05 +00:00
)
}
2026-04-01 01:14:38 +00:00
fn format_sandbox_report ( status : & runtime ::SandboxStatus ) -> String {
format! (
" Sandbox
Enabled { }
Active { }
Supported { }
In container { }
Requested ns { }
Active ns { }
Requested net { }
Active net { }
Filesystem mode { }
Filesystem active { }
Allowed mounts { }
Markers { }
Fallback reason { } " ,
status . enabled ,
status . active ,
status . supported ,
status . in_container ,
status . requested . namespace_restrictions ,
status . namespace_active ,
status . requested . network_isolation ,
status . network_active ,
status . filesystem_mode . as_str ( ) ,
status . filesystem_active ,
if status . allowed_mounts . is_empty ( ) {
" <none> " . to_string ( )
} else {
status . allowed_mounts . join ( " , " )
} ,
if status . container_markers . is_empty ( ) {
" <none> " . to_string ( )
} else {
status . container_markers . join ( " , " )
} ,
status
. fallback_reason
. clone ( )
. unwrap_or_else ( | | " <none> " . to_string ( ) ) ,
)
}
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
fn format_commit_preflight_report ( branch : Option < & str > , summary : GitWorkspaceSummary ) -> String {
format! (
" Commit
2026-04-02 18:10:32 +09:00
Result ready
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
Branch { }
Workspace { }
Changed files { }
2026-04-02 18:10:32 +09:00
Action create a git commit from the current workspace changes " ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
branch . unwrap_or ( " unknown " ) ,
summary . headline ( ) ,
summary . changed_files ,
)
}
fn format_commit_skipped_report ( ) -> String {
" Commit
Result skipped
Reason no workspace changes
2026-04-02 18:10:32 +09:00
Action create a git commit from the current workspace changes
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
Next / status to inspect context · / diff to inspect repo changes "
. to_string ( )
}
2026-04-02 07:44:39 +00:00
fn print_sandbox_status_snapshot ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let runtime_config = loader
. load ( )
. unwrap_or_else ( | _ | runtime ::RuntimeConfig ::empty ( ) ) ;
println! (
" {} " ,
format_sandbox_report ( & resolve_sandbox_status ( runtime_config . sandbox ( ) , & cwd ) )
) ;
Ok ( ( ) )
}
2026-03-31 21:11:57 +00:00
fn render_config_report ( section : Option < & str > ) -> Result < String , Box < dyn std ::error ::Error > > {
2026-03-31 19:54:09 +00:00
let cwd = env ::current_dir ( ) ? ;
let loader = ConfigLoader ::default_for ( & cwd ) ;
let discovered = loader . discover ( ) ;
let runtime_config = loader . load ( ) ? ;
2026-03-31 20:41:39 +00:00
let mut lines = vec! [
format! (
" Config
Working directory { }
Loaded files { }
Merged keys { } " ,
cwd . display ( ) ,
runtime_config . loaded_entries ( ) . len ( ) ,
runtime_config . merged ( ) . len ( )
) ,
" Discovered files " . to_string ( ) ,
] ;
2026-03-31 19:54:09 +00:00
for entry in discovered {
let source = match entry . source {
ConfigSource ::User = > " user " ,
ConfigSource ::Project = > " project " ,
ConfigSource ::Local = > " local " ,
} ;
let status = if runtime_config
. loaded_entries ( )
. iter ( )
. any ( | loaded_entry | loaded_entry . path = = entry . path )
{
" loaded "
} else {
" missing "
} ;
lines . push ( format! (
" {source:<7} {status:<7} {} " ,
entry . path . display ( )
) ) ;
}
2026-03-31 21:11:57 +00:00
if let Some ( section ) = section {
lines . push ( format! ( " Merged section: {section} " ) ) ;
let value = match section {
" env " = > runtime_config . get ( " env " ) ,
" hooks " = > runtime_config . get ( " hooks " ) ,
" model " = > runtime_config . get ( " model " ) ,
2026-04-01 04:30:28 +00:00
" plugins " = > runtime_config
. get ( " plugins " )
. or_else ( | | runtime_config . get ( " enabledPlugins " ) ) ,
2026-03-31 21:11:57 +00:00
other = > {
lines . push ( format! (
2026-04-01 04:30:28 +00:00
" Unsupported config section '{other}'. Use env, hooks, model, or plugins. "
2026-03-31 21:11:57 +00:00
) ) ;
return Ok ( lines . join (
"
" ,
) ) ;
}
} ;
lines . push ( format! (
" {} " ,
match value {
Some ( value ) = > value . render ( ) ,
None = > " <unset> " . to_string ( ) ,
}
) ) ;
return Ok ( lines . join (
"
" ,
) ) ;
}
2026-03-31 20:41:39 +00:00
lines . push ( " Merged JSON " . to_string ( ) ) ;
lines . push ( format! ( " {} " , runtime_config . as_json ( ) . render ( ) ) ) ;
2026-03-31 19:54:09 +00:00
Ok ( lines . join (
"
" ,
) )
}
fn render_memory_report ( ) -> Result < String , Box < dyn std ::error ::Error > > {
2026-03-31 21:08:19 +00:00
let cwd = env ::current_dir ( ) ? ;
let project_context = ProjectContext ::discover ( & cwd , DEFAULT_DATE ) ? ;
2026-03-31 19:54:09 +00:00
let mut lines = vec! [ format! (
2026-03-31 21:08:19 +00:00
" Memory
Working directory { }
Instruction files { } " ,
cwd . display ( ) ,
2026-03-31 19:54:09 +00:00
project_context . instruction_files . len ( )
) ] ;
if project_context . instruction_files . is_empty ( ) {
2026-03-31 21:08:19 +00:00
lines . push ( " Discovered files " . to_string ( ) ) ;
2026-03-31 19:54:09 +00:00
lines . push (
" No CLAUDE instruction files discovered in the current directory ancestry. "
. to_string ( ) ,
) ;
} else {
2026-03-31 21:08:19 +00:00
lines . push ( " Discovered files " . to_string ( ) ) ;
for ( index , file ) in project_context . instruction_files . iter ( ) . enumerate ( ) {
2026-03-31 19:54:09 +00:00
let preview = file . content . lines ( ) . next ( ) . unwrap_or ( " " ) . trim ( ) ;
let preview = if preview . is_empty ( ) {
" <empty> "
} else {
preview
} ;
2026-03-31 21:08:19 +00:00
lines . push ( format! ( " {} . {} " , index + 1 , file . path . display ( ) , ) ) ;
2026-03-31 19:54:09 +00:00
lines . push ( format! (
2026-03-31 21:08:19 +00:00
" lines={} preview={} " ,
2026-03-31 19:54:09 +00:00
file . content . lines ( ) . count ( ) ,
preview
) ) ;
}
}
Ok ( lines . join (
"
" ,
) )
}
2026-03-31 19:57:38 +00:00
fn init_claude_md ( ) -> Result < String , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
2026-04-01 01:14:44 +00:00
Ok ( initialize_repo ( & cwd ) ? . render ( ) )
2026-03-31 19:57:38 +00:00
}
2026-04-01 01:14:44 +00:00
fn run_init ( ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
println! ( " {} " , init_claude_md ( ) ? ) ;
Ok ( ( ) )
2026-03-31 19:57:38 +00:00
}
2026-03-31 19:27:31 +00:00
fn normalize_permission_mode ( mode : & str ) -> Option < & 'static str > {
match mode . trim ( ) {
" read-only " = > Some ( " read-only " ) ,
" workspace-write " = > Some ( " workspace-write " ) ,
" danger-full-access " = > Some ( " danger-full-access " ) ,
_ = > None ,
}
}
2026-03-31 22:49:50 +00:00
fn render_diff_report ( ) -> Result < String , Box < dyn std ::error ::Error > > {
2026-04-01 01:10:57 +00:00
render_diff_report_for ( & env ::current_dir ( ) ? )
}
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 ( ) {
2026-03-31 22:49:50 +00:00
return Ok (
" Diff \n Result clean working tree \n Detail no current changes "
. to_string ( ) ,
) ;
}
2026-04-01 01:10:57 +00:00
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 ) ? )
2026-03-31 22:49:50 +00:00
}
2026-04-01 03:55:00 +00:00
fn render_teleport_report ( target : & str ) -> Result < String , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let file_list = Command ::new ( " rg " )
. args ( [ " --files " ] )
. current_dir ( & cwd )
. output ( ) ? ;
let file_matches = if file_list . status . success ( ) {
String ::from_utf8 ( file_list . stdout ) ?
. lines ( )
. filter ( | line | line . contains ( target ) )
. take ( 10 )
. map ( ToOwned ::to_owned )
. collect ::< Vec < _ > > ( )
} else {
Vec ::new ( )
} ;
let content_output = Command ::new ( " rg " )
. args ( [ " -n " , " -S " , " --color " , " never " , target , " . " ] )
. current_dir ( & cwd )
. output ( ) ? ;
2026-04-02 18:10:32 +09:00
let mut lines = vec! [
" Teleport " . to_string ( ) ,
format! ( " Target {target} " ) ,
" Action search workspace files and content for the target " . to_string ( ) ,
] ;
2026-04-01 03:55:00 +00:00
if ! file_matches . is_empty ( ) {
lines . push ( String ::new ( ) ) ;
lines . push ( " File matches " . to_string ( ) ) ;
lines . extend ( file_matches . into_iter ( ) . map ( | path | format! ( " {path} " ) ) ) ;
}
if content_output . status . success ( ) {
let matches = String ::from_utf8 ( content_output . stdout ) ? ;
if ! matches . trim ( ) . is_empty ( ) {
lines . push ( String ::new ( ) ) ;
lines . push ( " Content matches " . to_string ( ) ) ;
lines . push ( truncate_for_prompt ( & matches , 4_000 ) ) ;
}
}
if lines . len ( ) = = 1 {
lines . push ( " Result no matches found " . to_string ( ) ) ;
}
Ok ( lines . join ( " \n " ) )
}
fn render_last_tool_debug_report ( session : & Session ) -> Result < String , Box < dyn std ::error ::Error > > {
let last_tool_use = session
. messages
. iter ( )
. rev ( )
. find_map ( | message | {
message . blocks . iter ( ) . rev ( ) . find_map ( | block | match block {
ContentBlock ::ToolUse { id , name , input } = > {
Some ( ( id . clone ( ) , name . clone ( ) , input . clone ( ) ) )
}
_ = > None ,
} )
} )
. ok_or_else ( | | " no prior tool call found in session " . to_string ( ) ) ? ;
let tool_result = session . messages . iter ( ) . rev ( ) . find_map ( | message | {
message . blocks . iter ( ) . rev ( ) . find_map ( | block | match block {
ContentBlock ::ToolResult {
tool_use_id ,
tool_name ,
output ,
is_error ,
} if tool_use_id = = & last_tool_use . 0 = > {
Some ( ( tool_name . clone ( ) , output . clone ( ) , * is_error ) )
}
_ = > None ,
} )
} ) ;
let mut lines = vec! [
" Debug tool call " . to_string ( ) ,
2026-04-02 18:10:32 +09:00
" Action inspect the last recorded tool call and its result " . to_string ( ) ,
2026-04-01 03:55:00 +00:00
format! ( " Tool id {} " , last_tool_use . 0 ) ,
format! ( " Tool name {} " , last_tool_use . 1 ) ,
" Input " . to_string ( ) ,
indent_block ( & last_tool_use . 2 , 4 ) ,
] ;
match tool_result {
Some ( ( tool_name , output , is_error ) ) = > {
lines . push ( " Result " . to_string ( ) ) ;
lines . push ( format! ( " name {tool_name} " ) ) ;
lines . push ( format! (
" status {} " ,
if is_error { " error " } else { " ok " }
) ) ;
lines . push ( indent_block ( & output , 4 ) ) ;
}
None = > lines . push ( " Result missing tool result " . to_string ( ) ) ,
}
Ok ( lines . join ( " \n " ) )
}
fn indent_block ( value : & str , spaces : usize ) -> String {
let indent = " " . repeat ( spaces ) ;
value
. lines ( )
. map ( | line | format! ( " {indent} {line} " ) )
. collect ::< Vec < _ > > ( )
. join ( " \n " )
}
2026-04-02 18:14:09 +09:00
fn validate_no_args (
command_name : & str ,
args : Option < & str > ,
) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2026-04-02 18:10:32 +09:00
if let Some ( args ) = args . map ( str ::trim ) . filter ( | value | ! value . is_empty ( ) ) {
return Err ( format! (
" {command_name} does not accept arguments. Received: {args} \n Usage: {command_name} "
)
. into ( ) ) ;
}
Ok ( ( ) )
}
fn format_bughunter_report ( scope : Option < & str > ) -> String {
format! (
" Bughunter
Scope { }
Action inspect the selected code for likely bugs and correctness issues
Output findings should include file paths , severity , and suggested fixes " ,
scope . unwrap_or ( " the current repository " )
)
}
fn format_ultraplan_report ( task : Option < & str > ) -> String {
format! (
" Ultraplan
Task { }
Action break work into a multi - step execution plan
Output plan should cover goals , risks , sequencing , verification , and rollback " ,
task . unwrap_or ( " the current repo work " )
)
}
fn format_pr_report ( branch : & str , context : Option < & str > ) -> String {
format! (
" PR
Branch { branch }
Context { }
Action draft or create a pull request for the current branch
Output title and markdown body suitable for GitHub " ,
context . unwrap_or ( " none " )
)
}
fn format_issue_report ( context : Option < & str > ) -> String {
format! (
" Issue
Context { }
Action draft or create a GitHub issue from the current context
Output title and markdown body suitable for GitHub " ,
context . unwrap_or ( " none " )
)
}
2026-04-01 03:55:00 +00:00
fn git_output ( args : & [ & str ] ) -> Result < String , Box < dyn std ::error ::Error > > {
let output = Command ::new ( " git " )
. args ( args )
. 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 {} failed: {stderr} " , args . join ( " " ) ) . into ( ) ) ;
}
Ok ( String ::from_utf8 ( output . stdout ) ? )
}
fn git_status_ok ( args : & [ & str ] ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let output = Command ::new ( " git " )
. args ( args )
. 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 {} failed: {stderr} " , args . join ( " " ) ) . into ( ) ) ;
}
Ok ( ( ) )
}
fn command_exists ( name : & str ) -> bool {
Command ::new ( " which " )
. arg ( name )
. output ( )
. map ( | output | output . status . success ( ) )
. unwrap_or ( false )
}
fn write_temp_text_file (
filename : & str ,
contents : & str ,
) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let path = env ::temp_dir ( ) . join ( filename ) ;
fs ::write ( & path , contents ) ? ;
Ok ( path )
}
fn recent_user_context ( session : & Session , limit : usize ) -> String {
let requests = session
. messages
. iter ( )
. filter ( | message | message . role = = MessageRole ::User )
. filter_map ( | message | {
message . blocks . iter ( ) . find_map ( | block | match block {
ContentBlock ::Text { text } = > Some ( text . trim ( ) . to_string ( ) ) ,
_ = > None ,
} )
} )
. rev ( )
. take ( limit )
. collect ::< Vec < _ > > ( ) ;
if requests . is_empty ( ) {
" <no prior user messages> " . to_string ( )
} else {
requests
. into_iter ( )
. rev ( )
. enumerate ( )
. map ( | ( index , text ) | format! ( " {} . {} " , index + 1 , text ) )
. collect ::< Vec < _ > > ( )
. join ( " \n " )
}
}
fn truncate_for_prompt ( value : & str , limit : usize ) -> String {
if value . chars ( ) . count ( ) < = limit {
value . trim ( ) . to_string ( )
} else {
let truncated = value . chars ( ) . take ( limit ) . collect ::< String > ( ) ;
format! ( " {} \n …[truncated] " , truncated . trim_end ( ) )
}
}
fn sanitize_generated_message ( value : & str ) -> String {
value . trim ( ) . trim_matches ( '`' ) . trim ( ) . replace ( " \r \n " , " \n " )
}
fn parse_titled_body ( value : & str ) -> Option < ( String , String ) > {
let normalized = sanitize_generated_message ( value ) ;
let title = normalized
. lines ( )
. find_map ( | line | line . strip_prefix ( " TITLE: " ) . map ( str ::trim ) ) ? ;
let body_start = normalized . find ( " BODY: " ) ? ;
let body = normalized [ body_start + " BODY: " . len ( ) .. ] . trim ( ) ;
Some ( ( title . to_string ( ) , body . to_string ( ) ) )
}
2026-03-31 22:49:50 +00:00
fn render_version_report ( ) -> String {
let git_sha = GIT_SHA . unwrap_or ( " unknown " ) ;
let target = BUILD_TARGET . unwrap_or ( " unknown " ) ;
format! (
2026-04-01 01:44:55 +00:00
" Claw Code \n Version {VERSION} \n Git SHA {git_sha} \n Target {target} \n Build date {DEFAULT_DATE} "
2026-03-31 22:49:50 +00:00
)
}
fn render_export_text ( session : & Session ) -> String {
let mut lines = vec! [ " # Conversation Export " . to_string ( ) , String ::new ( ) ] ;
for ( index , message ) in session . messages . iter ( ) . enumerate ( ) {
let role = match message . role {
MessageRole ::System = > " system " ,
MessageRole ::User = > " user " ,
MessageRole ::Assistant = > " assistant " ,
MessageRole ::Tool = > " tool " ,
} ;
lines . push ( format! ( " ## {} . {role} " , index + 1 ) ) ;
for block in & message . blocks {
match block {
ContentBlock ::Text { text } = > lines . push ( text . clone ( ) ) ,
ContentBlock ::ToolUse { id , name , input } = > {
lines . push ( format! ( " [tool_use id= {id} name= {name} ] {input} " ) ) ;
}
ContentBlock ::ToolResult {
tool_use_id ,
tool_name ,
output ,
is_error ,
} = > {
lines . push ( format! (
" [tool_result id={tool_use_id} name={tool_name} error={is_error}] {output} "
) ) ;
}
}
}
lines . push ( String ::new ( ) ) ;
}
lines . join ( " \n " )
}
fn default_export_filename ( session : & Session ) -> String {
let stem = session
. messages
. iter ( )
. find_map ( | message | match message . role {
MessageRole ::User = > message . blocks . iter ( ) . find_map ( | block | match block {
ContentBlock ::Text { text } = > Some ( text . as_str ( ) ) ,
_ = > None ,
} ) ,
_ = > None ,
} )
. map_or ( " conversation " , | text | {
text . lines ( ) . next ( ) . unwrap_or ( " conversation " )
} )
. chars ( )
. map ( | ch | {
if ch . is_ascii_alphanumeric ( ) {
ch . to_ascii_lowercase ( )
} else {
'-'
}
} )
. collect ::< String > ( )
. split ( '-' )
. filter ( | part | ! part . is_empty ( ) )
. take ( 8 )
. collect ::< Vec < _ > > ( )
. join ( " - " ) ;
let fallback = if stem . is_empty ( ) {
" conversation "
} else {
& stem
} ;
format! ( " {fallback} .txt " )
}
fn resolve_export_path (
requested_path : Option < & str > ,
session : & Session ,
) -> Result < PathBuf , Box < dyn std ::error ::Error > > {
let cwd = env ::current_dir ( ) ? ;
let file_name =
requested_path . map_or_else ( | | default_export_filename ( session ) , ToOwned ::to_owned ) ;
let final_name = if Path ::new ( & file_name )
. extension ( )
. is_some_and ( | ext | ext . eq_ignore_ascii_case ( " txt " ) )
{
file_name
} else {
format! ( " {file_name} .txt " )
} ;
Ok ( cwd . join ( final_name ) )
}
2026-03-31 18:39:39 +00:00
fn build_system_prompt ( ) -> Result < Vec < String > , Box < dyn std ::error ::Error > > {
Ok ( load_system_prompt (
env ::current_dir ( ) ? ,
DEFAULT_DATE ,
env ::consts ::OS ,
" unknown " ,
) ? )
}
2026-04-02 10:04:54 +00:00
fn build_runtime_plugin_state ( ) -> Result < RuntimePluginState , Box < dyn std ::error ::Error > > {
2026-04-01 03:35:25 +00:00
let cwd = env ::current_dir ( ) ? ;
2026-04-01 04:30:28 +00:00
let loader = ConfigLoader ::default_for ( & cwd ) ;
let runtime_config = loader . load ( ) ? ;
2026-04-02 10:04:54 +00:00
build_runtime_plugin_state_with_loader ( & cwd , & loader , & runtime_config )
}
fn build_runtime_plugin_state_with_loader (
cwd : & Path ,
loader : & ConfigLoader ,
runtime_config : & runtime ::RuntimeConfig ,
) -> Result < RuntimePluginState , Box < dyn std ::error ::Error > > {
2026-04-03 05:12:51 +09:00
let plugin_manager = build_plugin_manager ( cwd , loader , runtime_config ) ;
2026-04-02 10:04:54 +00:00
let plugin_registry = plugin_manager . plugin_registry ( ) ? ;
let plugin_hook_config =
runtime_hook_config_from_plugin_hooks ( plugin_registry . aggregated_hooks ( ) ? ) ;
let feature_config = runtime_config
. feature_config ( )
. clone ( )
. with_hooks ( runtime_config . hooks ( ) . merged ( & plugin_hook_config ) ) ;
2026-04-03 14:31:25 +00:00
let ( mcp_state , runtime_tools ) = build_runtime_mcp_state ( runtime_config ) ? ;
let tool_registry = GlobalToolRegistry ::with_plugin_tools ( plugin_registry . aggregated_tools ( ) ? ) ?
. with_runtime_tools ( runtime_tools ) ? ;
2026-04-02 10:04:54 +00:00
Ok ( RuntimePluginState {
feature_config ,
tool_registry ,
plugin_registry ,
2026-04-03 14:31:25 +00:00
mcp_state ,
2026-04-02 10:04:54 +00:00
} )
2026-04-01 03:35:25 +00:00
}
2026-04-01 04:40:19 +00:00
fn build_plugin_manager (
cwd : & Path ,
loader : & ConfigLoader ,
runtime_config : & runtime ::RuntimeConfig ,
) -> PluginManager {
let plugin_settings = runtime_config . plugins ( ) ;
let mut plugin_config = PluginManagerConfig ::new ( loader . config_home ( ) . to_path_buf ( ) ) ;
plugin_config . enabled_plugins = plugin_settings . enabled_plugins ( ) . clone ( ) ;
plugin_config . external_dirs = plugin_settings
. external_directories ( )
. iter ( )
. map ( | path | resolve_plugin_path ( cwd , loader . config_home ( ) , path ) )
. collect ( ) ;
plugin_config . install_root = plugin_settings
. install_root ( )
. map ( | path | resolve_plugin_path ( cwd , loader . config_home ( ) , path ) ) ;
plugin_config . registry_path = plugin_settings
. registry_path ( )
. map ( | path | resolve_plugin_path ( cwd , loader . config_home ( ) , path ) ) ;
plugin_config . bundled_root = plugin_settings
. bundled_root ( )
. map ( | path | resolve_plugin_path ( cwd , loader . config_home ( ) , path ) ) ;
PluginManager ::new ( plugin_config )
2026-03-31 18:39:39 +00:00
}
2026-04-01 04:40:19 +00:00
fn resolve_plugin_path ( cwd : & Path , config_home : & Path , value : & str ) -> PathBuf {
let path = PathBuf ::from ( value ) ;
if path . is_absolute ( ) {
path
} else if value . starts_with ( '.' ) {
cwd . join ( path )
} else {
config_home . join ( path )
}
2026-04-01 03:55:00 +00:00
}
2026-04-02 10:04:54 +00:00
fn runtime_hook_config_from_plugin_hooks ( hooks : PluginHooks ) -> runtime ::RuntimeHookConfig {
runtime ::RuntimeHookConfig ::new (
hooks . pre_tool_use ,
hooks . post_tool_use ,
hooks . post_tool_use_failure ,
)
}
2026-04-01 08:05:22 +00:00
#[ derive(Debug, Clone, PartialEq, Eq) ]
struct InternalPromptProgressState {
command_label : & 'static str ,
task_label : String ,
step : usize ,
phase : String ,
detail : Option < String > ,
saw_final_text : bool ,
}
#[ derive(Debug, Clone, Copy, PartialEq, Eq) ]
enum InternalPromptProgressEvent {
Started ,
Update ,
Heartbeat ,
Complete ,
Failed ,
}
#[ derive(Debug) ]
struct InternalPromptProgressShared {
state : Mutex < InternalPromptProgressState > ,
output_lock : Mutex < ( ) > ,
started_at : Instant ,
}
#[ derive(Debug, Clone) ]
struct InternalPromptProgressReporter {
shared : Arc < InternalPromptProgressShared > ,
}
#[ derive(Debug) ]
struct InternalPromptProgressRun {
reporter : InternalPromptProgressReporter ,
heartbeat_stop : Option < mpsc ::Sender < ( ) > > ,
heartbeat_handle : Option < thread ::JoinHandle < ( ) > > ,
}
impl InternalPromptProgressReporter {
fn ultraplan ( task : & str ) -> Self {
Self {
shared : Arc ::new ( InternalPromptProgressShared {
state : Mutex ::new ( InternalPromptProgressState {
command_label : " Ultraplan " ,
task_label : task . to_string ( ) ,
step : 0 ,
phase : " planning started " . to_string ( ) ,
detail : Some ( format! ( " task: {task} " ) ) ,
saw_final_text : false ,
} ) ,
output_lock : Mutex ::new ( ( ) ) ,
started_at : Instant ::now ( ) ,
} ) ,
}
}
fn emit ( & self , event : InternalPromptProgressEvent , error : Option < & str > ) {
let snapshot = self . snapshot ( ) ;
2026-04-01 08:10:23 +00:00
let line = format_internal_prompt_progress_line ( event , & snapshot , self . elapsed ( ) , error ) ;
2026-04-01 08:05:22 +00:00
self . write_line ( & line ) ;
}
fn mark_model_phase ( & self ) {
let snapshot = {
let mut state = self
. shared
. state
. lock ( )
. expect ( " internal prompt progress state poisoned " ) ;
state . step + = 1 ;
state . phase = if state . step = = 1 {
" analyzing request " . to_string ( )
} else {
" reviewing findings " . to_string ( )
} ;
state . detail = Some ( format! ( " task: {} " , state . task_label ) ) ;
state . clone ( )
} ;
self . write_line ( & format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Update ,
& snapshot ,
self . elapsed ( ) ,
None ,
) ) ;
}
fn mark_tool_phase ( & self , name : & str , input : & str ) {
let detail = describe_tool_progress ( name , input ) ;
let snapshot = {
let mut state = self
. shared
. state
. lock ( )
. expect ( " internal prompt progress state poisoned " ) ;
state . step + = 1 ;
state . phase = format! ( " running {name} " ) ;
state . detail = Some ( detail ) ;
state . clone ( )
} ;
self . write_line ( & format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Update ,
& snapshot ,
self . elapsed ( ) ,
None ,
) ) ;
}
fn mark_text_phase ( & self , text : & str ) {
let trimmed = text . trim ( ) ;
if trimmed . is_empty ( ) {
return ;
}
let detail = truncate_for_summary ( first_visible_line ( trimmed ) , 120 ) ;
let snapshot = {
let mut state = self
. shared
. state
. lock ( )
. expect ( " internal prompt progress state poisoned " ) ;
if state . saw_final_text {
return ;
}
state . saw_final_text = true ;
state . step + = 1 ;
state . phase = " drafting final plan " . to_string ( ) ;
state . detail = ( ! detail . is_empty ( ) ) . then_some ( detail ) ;
state . clone ( )
} ;
self . write_line ( & format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Update ,
& snapshot ,
self . elapsed ( ) ,
None ,
) ) ;
}
fn emit_heartbeat ( & self ) {
let snapshot = self . snapshot ( ) ;
self . write_line ( & format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Heartbeat ,
& snapshot ,
self . elapsed ( ) ,
None ,
) ) ;
}
fn snapshot ( & self ) -> InternalPromptProgressState {
self . shared
. state
. lock ( )
. expect ( " internal prompt progress state poisoned " )
. clone ( )
}
fn elapsed ( & self ) -> Duration {
self . shared . started_at . elapsed ( )
}
fn write_line ( & self , line : & str ) {
let _guard = self
. shared
. output_lock
. lock ( )
. expect ( " internal prompt progress output lock poisoned " ) ;
let mut stdout = io ::stdout ( ) ;
let _ = writeln! ( stdout , " {line} " ) ;
let _ = stdout . flush ( ) ;
}
}
impl InternalPromptProgressRun {
fn start_ultraplan ( task : & str ) -> Self {
let reporter = InternalPromptProgressReporter ::ultraplan ( task ) ;
reporter . emit ( InternalPromptProgressEvent ::Started , None ) ;
let ( heartbeat_stop , heartbeat_rx ) = mpsc ::channel ( ) ;
let heartbeat_reporter = reporter . clone ( ) ;
2026-04-01 08:10:23 +00:00
let heartbeat_handle = thread ::spawn ( move | | loop {
match heartbeat_rx . recv_timeout ( INTERNAL_PROGRESS_HEARTBEAT_INTERVAL ) {
Ok ( ( ) ) | Err ( RecvTimeoutError ::Disconnected ) = > break ,
Err ( RecvTimeoutError ::Timeout ) = > heartbeat_reporter . emit_heartbeat ( ) ,
2026-04-01 08:05:22 +00:00
}
} ) ;
Self {
reporter ,
heartbeat_stop : Some ( heartbeat_stop ) ,
heartbeat_handle : Some ( heartbeat_handle ) ,
}
}
fn reporter ( & self ) -> InternalPromptProgressReporter {
self . reporter . clone ( )
}
fn finish_success ( & mut self ) {
self . stop_heartbeat ( ) ;
2026-04-01 08:10:23 +00:00
self . reporter
. emit ( InternalPromptProgressEvent ::Complete , None ) ;
2026-04-01 08:05:22 +00:00
}
fn finish_failure ( & mut self , error : & str ) {
self . stop_heartbeat ( ) ;
self . reporter
. emit ( InternalPromptProgressEvent ::Failed , Some ( error ) ) ;
}
fn stop_heartbeat ( & mut self ) {
if let Some ( sender ) = self . heartbeat_stop . take ( ) {
let _ = sender . send ( ( ) ) ;
}
if let Some ( handle ) = self . heartbeat_handle . take ( ) {
let _ = handle . join ( ) ;
}
}
}
impl Drop for InternalPromptProgressRun {
fn drop ( & mut self ) {
self . stop_heartbeat ( ) ;
}
}
fn format_internal_prompt_progress_line (
event : InternalPromptProgressEvent ,
snapshot : & InternalPromptProgressState ,
elapsed : Duration ,
error : Option < & str > ,
) -> String {
let elapsed_seconds = elapsed . as_secs ( ) ;
let step_label = if snapshot . step = = 0 {
" current step pending " . to_string ( )
} else {
format! ( " current step {} " , snapshot . step )
} ;
let mut status_bits = vec! [ step_label , format! ( " phase {} " , snapshot . phase ) ] ;
2026-04-01 08:10:23 +00:00
if let Some ( detail ) = snapshot
. detail
. as_deref ( )
. filter ( | detail | ! detail . is_empty ( ) )
{
2026-04-01 08:05:22 +00:00
status_bits . push ( detail . to_string ( ) ) ;
}
let status = status_bits . join ( " · " ) ;
match event {
InternalPromptProgressEvent ::Started = > {
2026-04-01 08:10:23 +00:00
format! (
" 🧭 {} status · planning started · {status} " ,
snapshot . command_label
)
2026-04-01 08:05:22 +00:00
}
InternalPromptProgressEvent ::Update = > {
format! ( " … {} status · {status} " , snapshot . command_label )
}
InternalPromptProgressEvent ::Heartbeat = > format! (
" … {} heartbeat · {elapsed_seconds}s elapsed · {status} " ,
snapshot . command_label
) ,
InternalPromptProgressEvent ::Complete = > format! (
" ✔ {} status · completed · {elapsed_seconds}s elapsed · {} steps total " ,
2026-04-01 08:10:23 +00:00
snapshot . command_label , snapshot . step
2026-04-01 08:05:22 +00:00
) ,
InternalPromptProgressEvent ::Failed = > format! (
" ✘ {} status · failed · {elapsed_seconds}s elapsed · {} " ,
snapshot . command_label ,
error . unwrap_or ( " unknown error " )
) ,
}
}
fn describe_tool_progress ( name : & str , input : & str ) -> String {
let parsed : serde_json ::Value =
serde_json ::from_str ( input ) . unwrap_or ( serde_json ::Value ::String ( input . to_string ( ) ) ) ;
match name {
" bash " | " Bash " = > {
let command = parsed
. get ( " command " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
if command . is_empty ( ) {
" running shell command " . to_string ( )
} else {
format! ( " command {} " , truncate_for_summary ( command . trim ( ) , 100 ) )
}
}
" read_file " | " Read " = > format! ( " reading {} " , extract_tool_path ( & parsed ) ) ,
" write_file " | " Write " = > format! ( " writing {} " , extract_tool_path ( & parsed ) ) ,
" edit_file " | " Edit " = > format! ( " editing {} " , extract_tool_path ( & parsed ) ) ,
" glob_search " | " Glob " = > {
let pattern = parsed
. get ( " pattern " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " ? " ) ;
let scope = parsed
. get ( " path " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " . " ) ;
format! ( " glob ` {pattern} ` in {scope} " )
}
" grep_search " | " Grep " = > {
let pattern = parsed
. get ( " pattern " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " ? " ) ;
let scope = parsed
. get ( " path " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " . " ) ;
format! ( " grep ` {pattern} ` in {scope} " )
}
" web_search " | " WebSearch " = > parsed
. get ( " query " )
. and_then ( | value | value . as_str ( ) )
. map_or_else (
| | " running web search " . to_string ( ) ,
| query | format! ( " query {} " , truncate_for_summary ( query , 100 ) ) ,
) ,
_ = > {
let summary = summarize_tool_payload ( input ) ;
if summary . is_empty ( ) {
format! ( " running {name} " )
} else {
format! ( " {name} : {summary} " )
}
}
}
}
2026-04-01 04:30:28 +00:00
#[ allow(clippy::needless_pass_by_value) ]
2026-04-01 08:19:25 +00:00
#[ allow(clippy::too_many_arguments) ]
2026-04-01 03:55:00 +00:00
fn build_runtime (
session : Session ,
2026-04-01 04:30:29 +00:00
session_id : & str ,
2026-04-01 03:55:00 +00:00
model : String ,
system_prompt : Vec < String > ,
enable_tools : bool ,
emit_output : bool ,
allowed_tools : Option < AllowedToolSet > ,
permission_mode : PermissionMode ,
2026-04-01 08:05:22 +00:00
progress_reporter : Option < InternalPromptProgressReporter > ,
2026-04-02 10:04:54 +00:00
) -> Result < BuiltRuntime , Box < dyn std ::error ::Error > > {
let runtime_plugin_state = build_runtime_plugin_state ( ) ? ;
build_runtime_with_plugin_state (
session ,
session_id ,
model ,
system_prompt ,
enable_tools ,
emit_output ,
allowed_tools ,
permission_mode ,
progress_reporter ,
runtime_plugin_state ,
)
}
#[ allow(clippy::needless_pass_by_value) ]
#[ allow(clippy::too_many_arguments) ]
fn build_runtime_with_plugin_state (
session : Session ,
session_id : & str ,
model : String ,
system_prompt : Vec < String > ,
enable_tools : bool ,
emit_output : bool ,
allowed_tools : Option < AllowedToolSet > ,
permission_mode : PermissionMode ,
progress_reporter : Option < InternalPromptProgressReporter > ,
runtime_plugin_state : RuntimePluginState ,
) -> Result < BuiltRuntime , Box < dyn std ::error ::Error > > {
let RuntimePluginState {
feature_config ,
tool_registry ,
plugin_registry ,
2026-04-03 14:31:25 +00:00
mcp_state ,
2026-04-02 10:04:54 +00:00
} = runtime_plugin_state ;
plugin_registry . initialize ( ) ? ;
feat: ultraclaw session outputs — registry tests, MCP bridge, PARITY.md, cleanup
Ultraclaw mode results from 10 parallel opencode sessions:
- PARITY.md: Updated both copies with all 9 landed lanes, commit hashes,
line counts, and test counts. All checklist items marked complete.
- MCP bridge: McpToolRegistry.call_tool now wired to real McpServerManager
via async JSON-RPC (discover_tools -> tools/call -> shutdown)
- Registry tests: Added coverage for TaskRegistry, TeamRegistry,
CronRegistry, PermissionEnforcer, LspRegistry (branch-focused tests)
- Permissions refactor: Simplified authorize_with_context, extracted helpers,
added characterization tests (185 runtime tests pass)
- AI slop cleanup: Removed redundant comments, unused_self suppressions,
tightened unreachable branches
- CLI fixes: Minor adjustments in main.rs and hooks.rs
All 363+ tests pass. Workspace compiles clean.
2026-04-03 18:23:03 +09:00
let policy = permission_policy ( permission_mode , & feature_config , & tool_registry )
. map_err ( std ::io ::Error ::other ) ? ;
2026-04-01 04:50:26 +00:00
let mut runtime = ConversationRuntime ::new_with_features (
2026-04-01 03:55:00 +00:00
session ,
2026-04-01 06:50:18 +00:00
AnthropicRuntimeClient ::new (
2026-04-02 11:38:24 +09:00
session_id ,
2026-04-01 06:50:18 +00:00
model ,
enable_tools ,
emit_output ,
allowed_tools . clone ( ) ,
tool_registry . clone ( ) ,
2026-04-01 08:05:22 +00:00
progress_reporter ,
2026-04-01 06:50:18 +00:00
) ? ,
2026-04-03 14:31:25 +00:00
CliToolExecutor ::new (
allowed_tools . clone ( ) ,
emit_output ,
tool_registry . clone ( ) ,
mcp_state . clone ( ) ,
) ,
feat: ultraclaw session outputs — registry tests, MCP bridge, PARITY.md, cleanup
Ultraclaw mode results from 10 parallel opencode sessions:
- PARITY.md: Updated both copies with all 9 landed lanes, commit hashes,
line counts, and test counts. All checklist items marked complete.
- MCP bridge: McpToolRegistry.call_tool now wired to real McpServerManager
via async JSON-RPC (discover_tools -> tools/call -> shutdown)
- Registry tests: Added coverage for TaskRegistry, TeamRegistry,
CronRegistry, PermissionEnforcer, LspRegistry (branch-focused tests)
- Permissions refactor: Simplified authorize_with_context, extracted helpers,
added characterization tests (185 runtime tests pass)
- AI slop cleanup: Removed redundant comments, unused_self suppressions,
tightened unreachable branches
- CLI fixes: Minor adjustments in main.rs and hooks.rs
All 363+ tests pass. Workspace compiles clean.
2026-04-03 18:23:03 +09:00
policy ,
2026-04-01 03:55:00 +00:00
system_prompt ,
2026-04-02 11:16:56 +09:00
& feature_config ,
2026-04-01 04:40:18 +00:00
) ;
2026-04-01 04:50:26 +00:00
if emit_output {
runtime = runtime . with_hook_progress_reporter ( Box ::new ( CliHookProgressReporter ) ) ;
}
2026-04-03 14:31:25 +00:00
Ok ( BuiltRuntime ::new ( runtime , plugin_registry , mcp_state ) )
2026-03-31 18:39:39 +00:00
}
2026-04-01 04:50:26 +00:00
struct CliHookProgressReporter ;
impl runtime ::HookProgressReporter for CliHookProgressReporter {
fn on_event ( & mut self , event : & runtime ::HookProgressEvent ) {
match event {
runtime ::HookProgressEvent ::Started {
event ,
tool_name ,
command ,
} = > eprintln! (
" [hook {event_name}] {tool_name}: {command} " ,
event_name = event . as_str ( )
) ,
runtime ::HookProgressEvent ::Completed {
event ,
tool_name ,
command ,
} = > eprintln! (
" [hook done {event_name}] {tool_name}: {command} " ,
event_name = event . as_str ( )
) ,
runtime ::HookProgressEvent ::Cancelled {
event ,
tool_name ,
command ,
} = > eprintln! (
" [hook cancelled {event_name}] {tool_name}: {command} " ,
event_name = event . as_str ( )
) ,
}
}
2026-04-01 03:55:00 +00:00
}
struct CliPermissionPrompter {
current_mode : PermissionMode ,
2026-04-01 00:06:15 +00:00
}
impl CliPermissionPrompter {
fn new ( current_mode : PermissionMode ) -> Self {
Self { current_mode }
}
}
impl runtime ::PermissionPrompter for CliPermissionPrompter {
fn decide (
& mut self ,
request : & runtime ::PermissionRequest ,
) -> runtime ::PermissionPromptDecision {
println! ( ) ;
println! ( " Permission approval required " ) ;
println! ( " Tool {} " , request . tool_name ) ;
println! ( " Current mode {} " , self . current_mode . as_str ( ) ) ;
println! ( " Required mode {} " , request . required_mode . as_str ( ) ) ;
2026-04-01 04:40:18 +00:00
if let Some ( reason ) = & request . reason {
println! ( " Reason {reason} " ) ;
}
2026-04-01 00:06:15 +00:00
println! ( " Input {} " , request . input ) ;
print! ( " Approve this tool call? [y/N]: " ) ;
let _ = io ::stdout ( ) . flush ( ) ;
let mut response = String ::new ( ) ;
match io ::stdin ( ) . read_line ( & mut response ) {
Ok ( _ ) = > {
let normalized = response . trim ( ) . to_ascii_lowercase ( ) ;
if matches! ( normalized . as_str ( ) , " y " | " yes " ) {
runtime ::PermissionPromptDecision ::Allow
} else {
runtime ::PermissionPromptDecision ::Deny {
reason : format ! (
" tool '{}' denied by user approval prompt " ,
request . tool_name
) ,
}
}
}
Err ( error ) = > runtime ::PermissionPromptDecision ::Deny {
reason : format ! ( " permission approval failed: {error} " ) ,
} ,
}
}
}
2026-04-01 03:55:00 +00:00
struct AnthropicRuntimeClient {
2026-03-31 18:39:39 +00:00
runtime : tokio ::runtime ::Runtime ,
2026-04-01 03:55:00 +00:00
client : AnthropicClient ,
2026-03-31 18:39:39 +00:00
model : String ,
enable_tools : bool ,
2026-04-01 02:42:49 +00:00
emit_output : bool ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 06:50:18 +00:00
tool_registry : GlobalToolRegistry ,
2026-04-01 08:05:22 +00:00
progress_reporter : Option < InternalPromptProgressReporter > ,
2026-03-31 18:39:39 +00:00
}
2026-04-01 03:55:00 +00:00
impl AnthropicRuntimeClient {
2026-03-31 23:38:53 +00:00
fn new (
2026-04-02 11:38:24 +09:00
session_id : & str ,
2026-03-31 23:38:53 +00:00
model : String ,
enable_tools : bool ,
2026-04-01 02:42:49 +00:00
emit_output : bool ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 06:50:18 +00:00
tool_registry : GlobalToolRegistry ,
2026-04-01 08:05:22 +00:00
progress_reporter : Option < InternalPromptProgressReporter > ,
2026-03-31 23:38:53 +00:00
) -> Result < Self , Box < dyn std ::error ::Error > > {
2026-03-31 18:39:39 +00:00
Ok ( Self {
runtime : tokio ::runtime ::Runtime ::new ( ) ? ,
2026-04-01 03:55:00 +00:00
client : AnthropicClient ::from_auth ( resolve_cli_auth_source ( ) ? )
2026-04-02 11:38:24 +09:00
. with_base_url ( api ::read_base_url ( ) )
. with_prompt_cache ( PromptCache ::new ( session_id ) ) ,
2026-03-31 18:39:39 +00:00
model ,
enable_tools ,
2026-04-01 02:42:49 +00:00
emit_output ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-04-01 06:50:18 +00:00
tool_registry ,
2026-04-01 08:05:22 +00:00
progress_reporter ,
2026-03-31 18:39:39 +00:00
} )
}
}
2026-03-31 23:38:05 +00:00
fn resolve_cli_auth_source ( ) -> Result < AuthSource , Box < dyn std ::error ::Error > > {
2026-04-01 00:24:55 +00:00
Ok ( resolve_startup_auth_source ( | | {
let cwd = env ::current_dir ( ) . map_err ( api ::ApiError ::from ) ? ;
let config = ConfigLoader ::default_for ( & cwd ) . load ( ) . map_err ( | error | {
api ::ApiError ::Auth ( format! ( " failed to load runtime OAuth config: {error} " ) )
} ) ? ;
Ok ( config . oauth ( ) . cloned ( ) )
} ) ? )
2026-03-31 23:38:05 +00:00
}
2026-04-01 03:55:00 +00:00
impl ApiClient for AnthropicRuntimeClient {
2026-03-31 19:23:05 +00:00
#[ allow(clippy::too_many_lines) ]
2026-03-31 18:39:39 +00:00
fn stream ( & mut self , request : ApiRequest ) -> Result < Vec < AssistantEvent > , RuntimeError > {
2026-04-01 08:05:22 +00:00
if let Some ( progress_reporter ) = & self . progress_reporter {
progress_reporter . mark_model_phase ( ) ;
}
2026-03-31 18:39:39 +00:00
let message_request = MessageRequest {
model : self . model . clone ( ) ,
2026-04-01 02:14:20 +00:00
max_tokens : max_tokens_for_model ( & self . model ) ,
2026-03-31 18:39:39 +00:00
messages : convert_messages ( & request . messages ) ,
system : ( ! request . system_prompt . is_empty ( ) ) . then ( | | request . system_prompt . join ( " \n \n " ) ) ,
2026-04-01 06:50:18 +00:00
tools : self
. enable_tools
. then ( | | filter_tool_specs ( & self . tool_registry , self . allowed_tools . as_ref ( ) ) ) ,
2026-03-31 18:39:39 +00:00
tool_choice : self . enable_tools . then_some ( ToolChoice ::Auto ) ,
stream : true ,
} ;
self . runtime . block_on ( async {
let mut stream = self
. client
. stream_message ( & message_request )
. await
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
let mut stdout = io ::stdout ( ) ;
2026-04-01 02:42:49 +00:00
let mut sink = io ::sink ( ) ;
let out : & mut dyn Write = if self . emit_output {
& mut stdout
} else {
& mut sink
} ;
2026-04-01 03:14:45 +00:00
let renderer = TerminalRenderer ::new ( ) ;
let mut markdown_stream = MarkdownStreamState ::default ( ) ;
2026-03-31 18:39:39 +00:00
let mut events = Vec ::new ( ) ;
2026-04-01 03:55:00 +00:00
let mut pending_tool : Option < ( String , String , String ) > = None ;
2026-03-31 18:39:39 +00:00
let mut saw_stop = false ;
while let Some ( event ) = stream
. next_event ( )
. await
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ?
{
match event {
ApiStreamEvent ::MessageStart ( start ) = > {
for block in start . message . content {
2026-04-01 03:55:00 +00:00
push_output_block ( block , out , & mut events , & mut pending_tool , true ) ? ;
2026-03-31 18:39:39 +00:00
}
}
ApiStreamEvent ::ContentBlockStart ( start ) = > {
push_output_block (
start . content_block ,
2026-04-01 02:42:49 +00:00
out ,
2026-03-31 18:39:39 +00:00
& mut events ,
2026-04-01 03:55:00 +00:00
& mut pending_tool ,
2026-04-01 02:42:49 +00:00
true ,
2026-03-31 18:39:39 +00:00
) ? ;
}
ApiStreamEvent ::ContentBlockDelta ( delta ) = > match delta . delta {
ContentBlockDelta ::TextDelta { text } = > {
if ! text . is_empty ( ) {
2026-04-01 08:05:22 +00:00
if let Some ( progress_reporter ) = & self . progress_reporter {
progress_reporter . mark_text_phase ( & text ) ;
}
2026-04-01 03:14:45 +00:00
if let Some ( rendered ) = markdown_stream . push ( & renderer , & text ) {
write! ( out , " {rendered} " )
. and_then ( | ( ) | out . flush ( ) )
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
}
2026-03-31 18:39:39 +00:00
events . push ( AssistantEvent ::TextDelta ( text ) ) ;
}
}
ContentBlockDelta ::InputJsonDelta { partial_json } = > {
2026-04-01 03:55:00 +00:00
if let Some ( ( _ , _ , input ) ) = & mut pending_tool {
2026-03-31 18:39:39 +00:00
input . push_str ( & partial_json ) ;
}
}
2026-04-01 18:57:50 +09:00
ContentBlockDelta ::ThinkingDelta { .. }
| ContentBlockDelta ::SignatureDelta { .. } = > { }
2026-03-31 18:39:39 +00:00
} ,
2026-04-01 03:55:00 +00:00
ApiStreamEvent ::ContentBlockStop ( _ ) = > {
2026-04-01 03:14:45 +00:00
if let Some ( rendered ) = markdown_stream . flush ( & renderer ) {
write! ( out , " {rendered} " )
. and_then ( | ( ) | out . flush ( ) )
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
}
2026-04-01 03:55:00 +00:00
if let Some ( ( id , name , input ) ) = pending_tool . take ( ) {
2026-04-01 08:05:22 +00:00
if let Some ( progress_reporter ) = & self . progress_reporter {
progress_reporter . mark_tool_phase ( & name , & input ) ;
}
2026-04-01 02:24:18 +00:00
// Display tool call now that input is fully accumulated
2026-04-01 02:42:49 +00:00
writeln! ( out , " \n {} " , format_tool_call_start ( & name , & input ) )
. and_then ( | ( ) | out . flush ( ) )
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
2026-03-31 18:39:39 +00:00
events . push ( AssistantEvent ::ToolUse { id , name , input } ) ;
}
}
ApiStreamEvent ::MessageDelta ( delta ) = > {
2026-04-01 06:15:15 +00:00
events . push ( AssistantEvent ::Usage ( delta . usage . token_usage ( ) ) ) ;
2026-03-31 18:39:39 +00:00
}
ApiStreamEvent ::MessageStop ( _ ) = > {
saw_stop = true ;
2026-04-01 03:14:45 +00:00
if let Some ( rendered ) = markdown_stream . flush ( & renderer ) {
write! ( out , " {rendered} " )
. and_then ( | ( ) | out . flush ( ) )
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
}
2026-03-31 18:39:39 +00:00
events . push ( AssistantEvent ::MessageStop ) ;
}
}
}
2026-04-01 06:15:13 +00:00
push_prompt_cache_record ( & self . client , & mut events ) ;
2026-03-31 18:39:39 +00:00
if ! saw_stop
& & events . iter ( ) . any ( | event | {
matches! ( event , AssistantEvent ::TextDelta ( text ) if ! text . is_empty ( ) )
| | matches! ( event , AssistantEvent ::ToolUse { .. } )
} )
{
events . push ( AssistantEvent ::MessageStop ) ;
}
if events
. iter ( )
. any ( | event | matches! ( event , AssistantEvent ::MessageStop ) )
{
return Ok ( events ) ;
}
let response = self
. client
. send_message ( & MessageRequest {
stream : false ,
.. message_request . clone ( )
} )
. await
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
2026-04-01 06:15:13 +00:00
let mut events = response_to_events ( response , out ) ? ;
push_prompt_cache_record ( & self . client , & mut events ) ;
Ok ( events )
2026-03-31 18:39:39 +00:00
} )
}
}
2026-04-01 02:42:49 +00:00
fn final_assistant_text ( summary : & runtime ::TurnSummary ) -> String {
summary
. assistant_messages
. last ( )
. map ( | message | {
message
. blocks
. iter ( )
. filter_map ( | block | match block {
ContentBlock ::Text { text } = > Some ( text . as_str ( ) ) ,
_ = > None ,
} )
. collect ::< Vec < _ > > ( )
. join ( " " )
} )
. unwrap_or_default ( )
}
fn collect_tool_uses ( summary : & runtime ::TurnSummary ) -> Vec < serde_json ::Value > {
summary
. assistant_messages
. iter ( )
. flat_map ( | message | message . blocks . iter ( ) )
. filter_map ( | block | match block {
ContentBlock ::ToolUse { id , name , input } = > Some ( json! ( {
" id " : id ,
" name " : name ,
" input " : input ,
} ) ) ,
_ = > None ,
} )
. collect ( )
}
fn collect_tool_results ( summary : & runtime ::TurnSummary ) -> Vec < serde_json ::Value > {
summary
. tool_results
. iter ( )
. flat_map ( | message | message . blocks . iter ( ) )
. filter_map ( | block | match block {
ContentBlock ::ToolResult {
tool_use_id ,
tool_name ,
output ,
is_error ,
} = > Some ( json! ( {
" tool_use_id " : tool_use_id ,
" tool_name " : tool_name ,
" output " : output ,
" is_error " : is_error ,
} ) ) ,
_ = > None ,
} )
. collect ( )
}
2026-04-01 06:15:13 +00:00
fn collect_prompt_cache_events ( summary : & runtime ::TurnSummary ) -> Vec < serde_json ::Value > {
summary
. prompt_cache_events
. iter ( )
. map ( | event | {
json! ( {
" unexpected " : event . unexpected ,
" reason " : event . reason ,
" previous_cache_read_input_tokens " : event . previous_cache_read_input_tokens ,
" current_cache_read_input_tokens " : event . current_cache_read_input_tokens ,
" token_drop " : event . token_drop ,
} )
} )
. collect ( )
}
2026-04-02 07:19:14 +00:00
fn slash_command_completion_candidates_with_sessions (
model : & str ,
active_session_id : Option < & str > ,
recent_session_ids : Vec < String > ,
) -> Vec < String > {
let mut completions = BTreeSet ::new ( ) ;
for spec in slash_command_specs ( ) {
completions . insert ( format! ( " / {} " , spec . name ) ) ;
for alias in spec . aliases {
completions . insert ( format! ( " / {alias} " ) ) ;
}
}
for candidate in [
" /bughunter " ,
" /clear --confirm " ,
" /config " ,
" /config env " ,
" /config hooks " ,
" /config model " ,
" /config plugins " ,
2026-04-02 10:04:40 +00:00
" /mcp " ,
" /mcp list " ,
" /mcp show " ,
2026-04-02 07:19:14 +00:00
" /export " ,
" /issue " ,
" /model " ,
" /model opus " ,
" /model sonnet " ,
" /model haiku " ,
" /permissions " ,
" /permissions read-only " ,
" /permissions workspace-write " ,
" /permissions danger-full-access " ,
" /plugin list " ,
" /plugin install " ,
" /plugin enable " ,
" /plugin disable " ,
" /plugin uninstall " ,
" /plugin update " ,
" /plugins list " ,
" /pr " ,
" /resume " ,
" /session list " ,
" /session switch " ,
" /session fork " ,
" /teleport " ,
" /ultraplan " ,
" /agents help " ,
2026-04-02 10:04:40 +00:00
" /mcp help " ,
2026-04-02 07:19:14 +00:00
" /skills help " ,
] {
completions . insert ( candidate . to_string ( ) ) ;
}
if ! model . trim ( ) . is_empty ( ) {
completions . insert ( format! ( " /model {} " , resolve_model_alias ( model ) ) ) ;
completions . insert ( format! ( " /model {model} " ) ) ;
}
if let Some ( active_session_id ) = active_session_id . filter ( | value | ! value . trim ( ) . is_empty ( ) ) {
completions . insert ( format! ( " /resume {active_session_id} " ) ) ;
completions . insert ( format! ( " /session switch {active_session_id} " ) ) ;
}
for session_id in recent_session_ids
. into_iter ( )
. filter ( | value | ! value . trim ( ) . is_empty ( ) )
. take ( 10 )
{
completions . insert ( format! ( " /resume {session_id} " ) ) ;
completions . insert ( format! ( " /session switch {session_id} " ) ) ;
}
completions . into_iter ( ) . collect ( )
2026-04-01 00:14:38 +00:00
}
fn format_tool_call_start ( name : & str , input : & str ) -> String {
2026-04-01 02:20:59 +00:00
let parsed : serde_json ::Value =
serde_json ::from_str ( input ) . unwrap_or ( serde_json ::Value ::String ( input . to_string ( ) ) ) ;
let detail = match name {
2026-04-01 03:14:45 +00:00
" bash " | " Bash " = > format_bash_call ( & parsed ) ,
" read_file " | " Read " = > {
let path = extract_tool_path ( & parsed ) ;
format! ( " \x1b [2m📄 Reading {path} … \x1b [0m " )
}
2026-04-01 02:20:59 +00:00
" write_file " | " Write " = > {
2026-04-01 03:14:45 +00:00
let path = extract_tool_path ( & parsed ) ;
2026-04-01 02:20:59 +00:00
let lines = parsed
. get ( " content " )
2026-04-01 03:14:45 +00:00
. and_then ( | value | value . as_str ( ) )
. map_or ( 0 , | content | content . lines ( ) . count ( ) ) ;
format! ( " \x1b [1;32m✏️ Writing {path} \x1b [0m \x1b [2m( {lines} lines) \x1b [0m " )
2026-04-01 02:20:59 +00:00
}
" edit_file " | " Edit " = > {
2026-04-01 03:14:45 +00:00
let path = extract_tool_path ( & parsed ) ;
let old_value = parsed
. get ( " old_string " )
. or_else ( | | parsed . get ( " oldString " ) )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
let new_value = parsed
. get ( " new_string " )
. or_else ( | | parsed . get ( " newString " ) )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
format! (
" \x1b [1;33m📝 Editing {path} \x1b [0m{} " ,
format_patch_preview ( old_value , new_value )
. map ( | preview | format! ( " \n {preview} " ) )
. unwrap_or_default ( )
)
2026-04-01 02:20:59 +00:00
}
2026-04-01 03:14:45 +00:00
" glob_search " | " Glob " = > format_search_start ( " 🔎 Glob " , & parsed ) ,
" grep_search " | " Grep " = > format_search_start ( " 🔎 Grep " , & parsed ) ,
2026-04-01 02:20:59 +00:00
" web_search " | " WebSearch " = > parsed
. get ( " query " )
2026-04-01 03:14:45 +00:00
. and_then ( | value | value . as_str ( ) )
2026-04-01 02:20:59 +00:00
. unwrap_or ( " ? " )
. to_string ( ) ,
_ = > summarize_tool_payload ( input ) ,
} ;
2026-04-01 03:14:45 +00:00
let border = " ─ " . repeat ( name . len ( ) + 8 ) ;
2026-04-01 00:14:38 +00:00
format! (
2026-04-01 02:20:59 +00:00
" \x1b [38;5;245m╭─ \x1b [1;36m{name} \x1b [0;38;5;245m ─╮ \x1b [0m \n \x1b [38;5;245m│ \x1b [0m {detail} \n \x1b [38;5;245m╰{border}╯ \x1b [0m "
2026-04-01 00:14:38 +00:00
)
}
fn format_tool_result ( name : & str , output : & str , is_error : bool ) -> String {
2026-04-01 02:20:59 +00:00
let icon = if is_error {
" \x1b [1;31m✗ \x1b [0m "
} else {
" \x1b [1;32m✓ \x1b [0m "
} ;
2026-04-01 03:14:45 +00:00
if is_error {
let summary = truncate_for_summary ( output . trim ( ) , 160 ) ;
return if summary . is_empty ( ) {
format! ( " {icon} \x1b [38;5;245m {name} \x1b [0m " )
} else {
format! ( " {icon} \x1b [38;5;245m {name} \x1b [0m \n \x1b [38;5;203m {summary} \x1b [0m " )
} ;
}
let parsed : serde_json ::Value =
serde_json ::from_str ( output ) . unwrap_or ( serde_json ::Value ::String ( output . to_string ( ) ) ) ;
match name {
" bash " | " Bash " = > format_bash_result ( icon , & parsed ) ,
" read_file " | " Read " = > format_read_result ( icon , & parsed ) ,
" write_file " | " Write " = > format_write_result ( icon , & parsed ) ,
" edit_file " | " Edit " = > format_edit_result ( icon , & parsed ) ,
" glob_search " | " Glob " = > format_glob_result ( icon , & parsed ) ,
" grep_search " | " Grep " = > format_grep_result ( icon , & parsed ) ,
2026-04-01 07:53:03 +00:00
_ = > format_generic_tool_result ( icon , name , & parsed ) ,
2026-04-01 03:14:45 +00:00
}
}
2026-04-01 07:49:20 +00:00
const DISPLAY_TRUNCATION_NOTICE : & str =
" \x1b [2m… output truncated for display; full result preserved in session. \x1b [0m " ;
const READ_DISPLAY_MAX_LINES : usize = 80 ;
const READ_DISPLAY_MAX_CHARS : usize = 6_000 ;
const TOOL_OUTPUT_DISPLAY_MAX_LINES : usize = 60 ;
const TOOL_OUTPUT_DISPLAY_MAX_CHARS : usize = 4_000 ;
2026-04-01 03:14:45 +00:00
fn extract_tool_path ( parsed : & serde_json ::Value ) -> String {
parsed
. get ( " file_path " )
. or_else ( | | parsed . get ( " filePath " ) )
. or_else ( | | parsed . get ( " path " ) )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " ? " )
. to_string ( )
}
fn format_search_start ( label : & str , parsed : & serde_json ::Value ) -> String {
let pattern = parsed
. get ( " pattern " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " ? " ) ;
let scope = parsed
. get ( " path " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " . " ) ;
format! ( " {label} {pattern} \n \x1b [2min {scope} \x1b [0m " )
}
fn format_patch_preview ( old_value : & str , new_value : & str ) -> Option < String > {
if old_value . is_empty ( ) & & new_value . is_empty ( ) {
return None ;
}
Some ( format! (
" \x1b [38;5;203m- {} \x1b [0m \n \x1b [38;5;70m+ {} \x1b [0m " ,
truncate_for_summary ( first_visible_line ( old_value ) , 72 ) ,
truncate_for_summary ( first_visible_line ( new_value ) , 72 )
) )
}
fn format_bash_call ( parsed : & serde_json ::Value ) -> String {
let command = parsed
. get ( " command " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
if command . is_empty ( ) {
String ::new ( )
} else {
format! (
" \x1b [48;5;236;38;5;255m $ {} \x1b [0m " ,
truncate_for_summary ( command , 160 )
)
}
}
fn first_visible_line ( text : & str ) -> & str {
text . lines ( )
. find ( | line | ! line . trim ( ) . is_empty ( ) )
. unwrap_or ( text )
}
fn format_bash_result ( icon : & str , parsed : & serde_json ::Value ) -> String {
2026-04-01 06:15:13 +00:00
use std ::fmt ::Write as _ ;
2026-04-01 03:14:45 +00:00
let mut lines = vec! [ format! ( " {icon} \x1b [38;5;245mbash \x1b [0m " ) ] ;
if let Some ( task_id ) = parsed
. get ( " backgroundTaskId " )
. and_then ( | value | value . as_str ( ) )
{
2026-04-01 04:40:19 +00:00
write! ( & mut lines [ 0 ] , " backgrounded ({task_id}) " ) . expect ( " write to string " ) ;
2026-04-01 03:14:45 +00:00
} else if let Some ( status ) = parsed
. get ( " returnCodeInterpretation " )
. and_then ( | value | value . as_str ( ) )
. filter ( | status | ! status . is_empty ( ) )
{
2026-04-01 04:40:19 +00:00
write! ( & mut lines [ 0 ] , " {status} " ) . expect ( " write to string " ) ;
2026-04-01 03:14:45 +00:00
}
if let Some ( stdout ) = parsed . get ( " stdout " ) . and_then ( | value | value . as_str ( ) ) {
if ! stdout . trim ( ) . is_empty ( ) {
2026-04-01 07:49:20 +00:00
lines . push ( truncate_output_for_display (
stdout ,
TOOL_OUTPUT_DISPLAY_MAX_LINES ,
TOOL_OUTPUT_DISPLAY_MAX_CHARS ,
) ) ;
2026-04-01 03:14:45 +00:00
}
}
if let Some ( stderr ) = parsed . get ( " stderr " ) . and_then ( | value | value . as_str ( ) ) {
if ! stderr . trim ( ) . is_empty ( ) {
2026-04-01 07:49:20 +00:00
lines . push ( format! (
" \x1b [38;5;203m{} \x1b [0m " ,
truncate_output_for_display (
stderr ,
TOOL_OUTPUT_DISPLAY_MAX_LINES ,
TOOL_OUTPUT_DISPLAY_MAX_CHARS ,
)
) ) ;
2026-04-01 03:14:45 +00:00
}
}
lines . join ( " \n \n " )
}
fn format_read_result ( icon : & str , parsed : & serde_json ::Value ) -> String {
let file = parsed . get ( " file " ) . unwrap_or ( parsed ) ;
let path = extract_tool_path ( file ) ;
let start_line = file
. get ( " startLine " )
2026-04-01 04:40:19 +00:00
. and_then ( serde_json ::Value ::as_u64 )
2026-04-01 03:14:45 +00:00
. unwrap_or ( 1 ) ;
let num_lines = file
. get ( " numLines " )
2026-04-01 04:40:19 +00:00
. and_then ( serde_json ::Value ::as_u64 )
2026-04-01 03:14:45 +00:00
. unwrap_or ( 0 ) ;
let total_lines = file
. get ( " totalLines " )
2026-04-01 04:40:19 +00:00
. and_then ( serde_json ::Value ::as_u64 )
2026-04-01 03:14:45 +00:00
. unwrap_or ( num_lines ) ;
let content = file
. get ( " content " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
let end_line = start_line . saturating_add ( num_lines . saturating_sub ( 1 ) ) ;
format! (
" {icon} \x1b [2m📄 Read {path} (lines {}-{} of {}) \x1b [0m \n {} " ,
start_line ,
end_line . max ( start_line ) ,
total_lines ,
2026-04-01 07:49:20 +00:00
truncate_output_for_display ( content , READ_DISPLAY_MAX_LINES , READ_DISPLAY_MAX_CHARS )
2026-04-01 03:14:45 +00:00
)
}
fn format_write_result ( icon : & str , parsed : & serde_json ::Value ) -> String {
let path = extract_tool_path ( parsed ) ;
let kind = parsed
. get ( " type " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or ( " write " ) ;
let line_count = parsed
. get ( " content " )
. and_then ( | value | value . as_str ( ) )
2026-04-01 04:40:19 +00:00
. map_or ( 0 , | content | content . lines ( ) . count ( ) ) ;
2026-04-01 03:14:45 +00:00
format! (
" {icon} \x1b [1;32m✏️ {} {path} \x1b [0m \x1b [2m({line_count} lines) \x1b [0m " ,
if kind = = " create " { " Wrote " } else { " Updated " } ,
)
}
fn format_structured_patch_preview ( parsed : & serde_json ::Value ) -> Option < String > {
let hunks = parsed . get ( " structuredPatch " ) ? . as_array ( ) ? ;
let mut preview = Vec ::new ( ) ;
for hunk in hunks . iter ( ) . take ( 2 ) {
let lines = hunk . get ( " lines " ) ? . as_array ( ) ? ;
for line in lines . iter ( ) . filter_map ( | value | value . as_str ( ) ) . take ( 6 ) {
match line . chars ( ) . next ( ) {
Some ( '+' ) = > preview . push ( format! ( " \x1b [38;5;70m {line} \x1b [0m " ) ) ,
Some ( '-' ) = > preview . push ( format! ( " \x1b [38;5;203m {line} \x1b [0m " ) ) ,
_ = > preview . push ( line . to_string ( ) ) ,
}
}
}
if preview . is_empty ( ) {
None
} else {
Some ( preview . join ( " \n " ) )
}
}
fn format_edit_result ( icon : & str , parsed : & serde_json ::Value ) -> String {
let path = extract_tool_path ( parsed ) ;
let suffix = if parsed
. get ( " replaceAll " )
2026-04-01 04:40:19 +00:00
. and_then ( serde_json ::Value ::as_bool )
2026-04-01 03:14:45 +00:00
. unwrap_or ( false )
{
" (replace all) "
} else {
" "
} ;
let preview = format_structured_patch_preview ( parsed ) . or_else ( | | {
let old_value = parsed
. get ( " oldString " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
let new_value = parsed
. get ( " newString " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
format_patch_preview ( old_value , new_value )
} ) ;
match preview {
Some ( preview ) = > format! ( " {icon} \x1b [1;33m📝 Edited {path} {suffix} \x1b [0m \n {preview} " ) ,
None = > format! ( " {icon} \x1b [1;33m📝 Edited {path} {suffix} \x1b [0m " ) ,
}
}
fn format_glob_result ( icon : & str , parsed : & serde_json ::Value ) -> String {
let num_files = parsed
. get ( " numFiles " )
2026-04-01 04:40:19 +00:00
. and_then ( serde_json ::Value ::as_u64 )
2026-04-01 03:14:45 +00:00
. unwrap_or ( 0 ) ;
let filenames = parsed
. get ( " filenames " )
. and_then ( | value | value . as_array ( ) )
. map ( | files | {
files
. iter ( )
. filter_map ( | value | value . as_str ( ) )
. take ( 8 )
. collect ::< Vec < _ > > ( )
. join ( " \n " )
} )
. unwrap_or_default ( ) ;
if filenames . is_empty ( ) {
format! ( " {icon} \x1b [38;5;245mglob_search \x1b [0m matched {num_files} files " )
} else {
format! ( " {icon} \x1b [38;5;245mglob_search \x1b [0m matched {num_files} files \n {filenames} " )
}
}
fn format_grep_result ( icon : & str , parsed : & serde_json ::Value ) -> String {
let num_matches = parsed
. get ( " numMatches " )
2026-04-01 04:40:19 +00:00
. and_then ( serde_json ::Value ::as_u64 )
2026-04-01 03:14:45 +00:00
. unwrap_or ( 0 ) ;
let num_files = parsed
. get ( " numFiles " )
2026-04-01 04:40:19 +00:00
. and_then ( serde_json ::Value ::as_u64 )
2026-04-01 03:14:45 +00:00
. unwrap_or ( 0 ) ;
let content = parsed
. get ( " content " )
. and_then ( | value | value . as_str ( ) )
. unwrap_or_default ( ) ;
let filenames = parsed
. get ( " filenames " )
. and_then ( | value | value . as_array ( ) )
. map ( | files | {
files
. iter ( )
. filter_map ( | value | value . as_str ( ) )
. take ( 8 )
. collect ::< Vec < _ > > ( )
. join ( " \n " )
} )
. unwrap_or_default ( ) ;
let summary = format! (
" {icon} \x1b [38;5;245mgrep_search \x1b [0m {num_matches} matches across {num_files} files "
) ;
if ! content . trim ( ) . is_empty ( ) {
2026-04-01 07:49:20 +00:00
format! (
" {summary} \n {} " ,
truncate_output_for_display (
content ,
TOOL_OUTPUT_DISPLAY_MAX_LINES ,
TOOL_OUTPUT_DISPLAY_MAX_CHARS ,
)
)
2026-04-01 03:14:45 +00:00
} else if ! filenames . is_empty ( ) {
format! ( " {summary} \n {filenames} " )
} else {
summary
}
2026-04-01 00:14:38 +00:00
}
2026-04-01 07:53:03 +00:00
fn format_generic_tool_result ( icon : & str , name : & str , parsed : & serde_json ::Value ) -> String {
let rendered_output = match parsed {
serde_json ::Value ::String ( text ) = > text . clone ( ) ,
serde_json ::Value ::Null = > String ::new ( ) ,
serde_json ::Value ::Object ( _ ) | serde_json ::Value ::Array ( _ ) = > {
serde_json ::to_string_pretty ( parsed ) . unwrap_or_else ( | _ | parsed . to_string ( ) )
}
_ = > parsed . to_string ( ) ,
} ;
let preview = truncate_output_for_display (
& rendered_output ,
TOOL_OUTPUT_DISPLAY_MAX_LINES ,
TOOL_OUTPUT_DISPLAY_MAX_CHARS ,
) ;
if preview . is_empty ( ) {
format! ( " {icon} \x1b [38;5;245m {name} \x1b [0m " )
} else if preview . contains ( '\n' ) {
format! ( " {icon} \x1b [38;5;245m {name} \x1b [0m \n {preview} " )
} else {
format! ( " {icon} \x1b [38;5;245m {name} : \x1b [0m {preview} " )
}
}
2026-04-01 00:14:38 +00:00
fn summarize_tool_payload ( payload : & str ) -> String {
let compact = match serde_json ::from_str ::< serde_json ::Value > ( payload ) {
Ok ( value ) = > value . to_string ( ) ,
Err ( _ ) = > payload . trim ( ) . to_string ( ) ,
} ;
truncate_for_summary ( & compact , 96 )
}
fn truncate_for_summary ( value : & str , limit : usize ) -> String {
let mut chars = value . chars ( ) ;
let truncated = chars . by_ref ( ) . take ( limit ) . collect ::< String > ( ) ;
if chars . next ( ) . is_some ( ) {
format! ( " {truncated} … " )
} else {
truncated
}
}
2026-04-01 07:49:20 +00:00
fn truncate_output_for_display ( content : & str , max_lines : usize , max_chars : usize ) -> String {
let original = content . trim_end_matches ( '\n' ) ;
if original . is_empty ( ) {
return String ::new ( ) ;
}
let mut preview_lines = Vec ::new ( ) ;
let mut used_chars = 0 usize ;
let mut truncated = false ;
for ( index , line ) in original . lines ( ) . enumerate ( ) {
if index > = max_lines {
truncated = true ;
break ;
}
let newline_cost = usize ::from ( ! preview_lines . is_empty ( ) ) ;
let available = max_chars . saturating_sub ( used_chars + newline_cost ) ;
if available = = 0 {
truncated = true ;
break ;
}
let line_chars = line . chars ( ) . count ( ) ;
if line_chars > available {
preview_lines . push ( line . chars ( ) . take ( available ) . collect ::< String > ( ) ) ;
truncated = true ;
break ;
}
preview_lines . push ( line . to_string ( ) ) ;
used_chars + = newline_cost + line_chars ;
}
let mut preview = preview_lines . join ( " \n " ) ;
if truncated {
if ! preview . is_empty ( ) {
preview . push ( '\n' ) ;
}
preview . push_str ( DISPLAY_TRUNCATION_NOTICE ) ;
}
preview
}
2026-03-31 18:39:39 +00:00
fn push_output_block (
block : OutputContentBlock ,
2026-04-01 02:42:49 +00:00
out : & mut ( impl Write + ? Sized ) ,
2026-03-31 18:39:39 +00:00
events : & mut Vec < AssistantEvent > ,
2026-04-01 03:55:00 +00:00
pending_tool : & mut Option < ( String , String , String ) > ,
2026-04-01 02:42:49 +00:00
streaming_tool_input : bool ,
2026-03-31 18:39:39 +00:00
) -> Result < ( ) , RuntimeError > {
match block {
OutputContentBlock ::Text { text } = > {
if ! text . is_empty ( ) {
2026-04-01 03:14:45 +00:00
let rendered = TerminalRenderer ::new ( ) . markdown_to_ansi ( & text ) ;
write! ( out , " {rendered} " )
2026-03-31 19:23:05 +00:00
. and_then ( | ( ) | out . flush ( ) )
2026-03-31 18:39:39 +00:00
. map_err ( | error | RuntimeError ::new ( error . to_string ( ) ) ) ? ;
events . push ( AssistantEvent ::TextDelta ( text ) ) ;
}
}
OutputContentBlock ::ToolUse { id , name , input } = > {
2026-04-01 02:24:18 +00:00
// During streaming, the initial content_block_start has an empty input ({}).
2026-04-01 02:42:49 +00:00
// The real input arrives via input_json_delta events. In
// non-streaming responses, preserve a legitimate empty object.
let initial_input = if streaming_tool_input
& & input . is_object ( )
& & input . as_object ( ) . is_some_and ( serde_json ::Map ::is_empty )
{
2026-04-01 02:24:18 +00:00
String ::new ( )
} else {
input . to_string ( )
} ;
2026-04-01 03:55:00 +00:00
* pending_tool = Some ( ( id , name , initial_input ) ) ;
2026-03-31 18:39:39 +00:00
}
2026-04-01 18:57:50 +09:00
OutputContentBlock ::Thinking { .. } | OutputContentBlock ::RedactedThinking { .. } = > { }
2026-03-31 18:39:39 +00:00
}
Ok ( ( ) )
}
fn response_to_events (
response : MessageResponse ,
2026-04-01 02:42:49 +00:00
out : & mut ( impl Write + ? Sized ) ,
2026-03-31 18:39:39 +00:00
) -> Result < Vec < AssistantEvent > , RuntimeError > {
let mut events = Vec ::new ( ) ;
2026-04-01 03:55:00 +00:00
let mut pending_tool = None ;
2026-03-31 18:39:39 +00:00
2026-04-01 03:55:00 +00:00
for block in response . content {
push_output_block ( block , out , & mut events , & mut pending_tool , false ) ? ;
if let Some ( ( id , name , input ) ) = pending_tool . take ( ) {
2026-03-31 18:39:39 +00:00
events . push ( AssistantEvent ::ToolUse { id , name , input } ) ;
}
}
2026-04-01 06:15:15 +00:00
events . push ( AssistantEvent ::Usage ( response . usage . token_usage ( ) ) ) ;
2026-03-31 18:39:39 +00:00
events . push ( AssistantEvent ::MessageStop ) ;
Ok ( events )
}
2026-04-02 11:38:24 +09:00
fn push_prompt_cache_record ( client : & AnthropicClient , events : & mut Vec < AssistantEvent > ) {
if let Some ( record ) = client . take_last_prompt_cache_record ( ) {
if let Some ( event ) = prompt_cache_record_to_runtime_event ( record ) {
events . push ( AssistantEvent ::PromptCache ( event ) ) ;
}
}
}
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
fn prompt_cache_record_to_runtime_event (
record : api ::PromptCacheRecord ,
) -> Option < PromptCacheEvent > {
2026-04-02 11:38:24 +09:00
let cache_break = record . cache_break ? ;
Some ( PromptCacheEvent {
unexpected : cache_break . unexpected ,
reason : cache_break . reason ,
previous_cache_read_input_tokens : cache_break . previous_cache_read_input_tokens ,
current_cache_read_input_tokens : cache_break . current_cache_read_input_tokens ,
token_drop : cache_break . token_drop ,
} )
}
2026-04-01 06:15:13 +00:00
2026-03-31 18:39:39 +00:00
struct CliToolExecutor {
renderer : TerminalRenderer ,
2026-04-01 02:42:49 +00:00
emit_output : bool ,
2026-03-31 23:38:53 +00:00
allowed_tools : Option < AllowedToolSet > ,
2026-04-01 06:50:18 +00:00
tool_registry : GlobalToolRegistry ,
2026-04-03 14:31:25 +00:00
mcp_state : Option < Arc < Mutex < RuntimeMcpState > > > ,
2026-03-31 18:39:39 +00:00
}
impl CliToolExecutor {
2026-04-01 06:50:18 +00:00
fn new (
allowed_tools : Option < AllowedToolSet > ,
emit_output : bool ,
tool_registry : GlobalToolRegistry ,
2026-04-03 14:31:25 +00:00
mcp_state : Option < Arc < Mutex < RuntimeMcpState > > > ,
2026-04-01 06:50:18 +00:00
) -> Self {
2026-03-31 18:39:39 +00:00
Self {
renderer : TerminalRenderer ::new ( ) ,
2026-04-01 02:42:49 +00:00
emit_output ,
2026-03-31 23:38:53 +00:00
allowed_tools ,
2026-04-01 06:50:18 +00:00
tool_registry ,
2026-04-03 14:31:25 +00:00
mcp_state ,
}
}
fn execute_search_tool ( & self , value : serde_json ::Value ) -> Result < String , ToolError > {
let input : ToolSearchRequest = serde_json ::from_value ( value )
. map_err ( | error | ToolError ::new ( format! ( " invalid tool input JSON: {error} " ) ) ) ? ;
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
let ( pending_mcp_servers , mcp_degraded ) = self
. mcp_state
. as_ref ( )
. map ( | state | {
let state = state
. lock ( )
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner ) ;
( state . pending_servers ( ) , state . degraded_report ( ) )
} )
. unwrap_or ( ( None , None ) ) ;
2026-04-03 14:31:25 +00:00
serde_json ::to_string_pretty ( & self . tool_registry . search (
& input . query ,
input . max_results . unwrap_or ( 5 ) ,
pending_mcp_servers ,
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
mcp_degraded ,
2026-04-03 14:31:25 +00:00
) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) )
}
fn execute_runtime_tool (
& self ,
tool_name : & str ,
value : serde_json ::Value ,
) -> Result < String , ToolError > {
let Some ( mcp_state ) = & self . mcp_state else {
return Err ( ToolError ::new ( format! (
" runtime tool `{tool_name}` is unavailable without configured MCP servers "
) ) ) ;
} ;
let mut mcp_state = mcp_state
. lock ( )
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner ) ;
match tool_name {
" MCPTool " = > {
let input : McpToolRequest = serde_json ::from_value ( value )
. map_err ( | error | ToolError ::new ( format! ( " invalid tool input JSON: {error} " ) ) ) ? ;
let qualified_name = input
. qualified_name
. or ( input . tool )
. ok_or_else ( | | ToolError ::new ( " missing required field `qualifiedName` " ) ) ? ;
mcp_state . call_tool ( & qualified_name , input . arguments )
}
" ListMcpResourcesTool " = > {
let input : ListMcpResourcesRequest = serde_json ::from_value ( value )
. map_err ( | error | ToolError ::new ( format! ( " invalid tool input JSON: {error} " ) ) ) ? ;
match input . server {
Some ( server_name ) = > mcp_state . list_resources_for_server ( & server_name ) ,
None = > mcp_state . list_resources_for_all_servers ( ) ,
}
}
" ReadMcpResourceTool " = > {
let input : ReadMcpResourceRequest = serde_json ::from_value ( value )
. map_err ( | error | ToolError ::new ( format! ( " invalid tool input JSON: {error} " ) ) ) ? ;
mcp_state . read_resource ( & input . server , & input . uri )
}
_ = > mcp_state . call_tool ( tool_name , Some ( value ) ) ,
2026-03-31 18:39:39 +00:00
}
}
}
impl ToolExecutor for CliToolExecutor {
fn execute ( & mut self , tool_name : & str , input : & str ) -> Result < String , ToolError > {
2026-03-31 23:38:53 +00:00
if self
. allowed_tools
. as_ref ( )
. is_some_and ( | allowed | ! allowed . contains ( tool_name ) )
{
return Err ( ToolError ::new ( format! (
" tool `{tool_name}` is not enabled by the current --allowedTools setting "
) ) ) ;
}
2026-03-31 18:39:39 +00:00
let value = serde_json ::from_str ( input )
. map_err ( | error | ToolError ::new ( format! ( " invalid tool input JSON: {error} " ) ) ) ? ;
2026-04-03 14:31:25 +00:00
let result = if tool_name = = " ToolSearch " {
self . execute_search_tool ( value )
} else if self . tool_registry . has_runtime_tool ( tool_name ) {
self . execute_runtime_tool ( tool_name , value )
} else {
self . tool_registry
. execute ( tool_name , & value )
. map_err ( ToolError ::new )
} ;
match result {
2026-03-31 18:39:39 +00:00
Ok ( output ) = > {
2026-04-01 02:42:49 +00:00
if self . emit_output {
let markdown = format_tool_result ( tool_name , & output , false ) ;
self . renderer
. stream_markdown ( & markdown , & mut io ::stdout ( ) )
. map_err ( | error | ToolError ::new ( error . to_string ( ) ) ) ? ;
}
2026-03-31 18:39:39 +00:00
Ok ( output )
}
2026-04-01 00:14:38 +00:00
Err ( error ) = > {
2026-04-01 02:42:49 +00:00
if self . emit_output {
2026-04-03 14:31:25 +00:00
let markdown = format_tool_result ( tool_name , & error . to_string ( ) , true ) ;
2026-04-01 02:42:49 +00:00
self . renderer
. stream_markdown ( & markdown , & mut io ::stdout ( ) )
. map_err ( | stream_error | ToolError ::new ( stream_error . to_string ( ) ) ) ? ;
}
2026-04-03 14:31:25 +00:00
Err ( error )
2026-04-01 00:14:38 +00:00
}
2026-03-31 18:39:39 +00:00
}
}
}
2026-04-01 04:30:25 +00:00
fn permission_policy (
mode : PermissionMode ,
feature_config : & runtime ::RuntimeFeatureConfig ,
2026-04-02 11:05:03 +09:00
tool_registry : & GlobalToolRegistry ,
2026-04-02 18:04:55 +09:00
) -> Result < PermissionPolicy , String > {
Ok ( tool_registry . permission_specs ( None ) ? . into_iter ( ) . fold (
2026-04-01 04:30:25 +00:00
PermissionPolicy ::new ( mode ) . with_permission_rules ( feature_config . permission_rules ( ) ) ,
2026-04-01 06:50:18 +00:00
| policy , ( name , required_permission ) | {
policy . with_tool_requirement ( name , required_permission )
} ,
2026-04-02 18:04:55 +09:00
) )
2026-03-31 18:39:39 +00:00
}
fn convert_messages ( messages : & [ ConversationMessage ] ) -> Vec < InputMessage > {
messages
. iter ( )
. filter_map ( | message | {
let role = match message . role {
MessageRole ::System | MessageRole ::User | MessageRole ::Tool = > " user " ,
MessageRole ::Assistant = > " assistant " ,
} ;
let content = message
. blocks
. iter ( )
. map ( | block | match block {
ContentBlock ::Text { text } = > InputContentBlock ::Text { text : text . clone ( ) } ,
ContentBlock ::ToolUse { id , name , input } = > InputContentBlock ::ToolUse {
id : id . clone ( ) ,
name : name . clone ( ) ,
input : serde_json ::from_str ( input )
. unwrap_or_else ( | _ | serde_json ::json! ( { " raw " : input } ) ) ,
} ,
ContentBlock ::ToolResult {
tool_use_id ,
output ,
is_error ,
..
} = > InputContentBlock ::ToolResult {
tool_use_id : tool_use_id . clone ( ) ,
content : vec ! [ ToolResultContentBlock ::Text {
text : output . clone ( ) ,
} ] ,
is_error : * is_error ,
} ,
} )
. collect ::< Vec < _ > > ( ) ;
( ! content . is_empty ( ) ) . then ( | | InputMessage {
role : role . to_string ( ) ,
content ,
} )
} )
. collect ( )
}
2026-04-02 07:15:03 +00:00
#[ allow(clippy::too_many_lines) ]
2026-04-01 01:14:44 +00:00
fn print_help_to ( out : & mut impl Write ) -> io ::Result < ( ) > {
2026-04-01 01:44:55 +00:00
writeln! ( out , " claw v{VERSION} " ) ? ;
2026-04-01 01:14:44 +00:00
writeln! ( out ) ? ;
writeln! ( out , " Usage: " ) ? ;
writeln! (
out ,
2026-04-01 01:44:55 +00:00
" claw [--model MODEL] [--allowedTools TOOL[,TOOL...]] "
2026-04-01 01:14:44 +00:00
) ? ;
writeln! ( out , " Start the interactive REPL " ) ? ;
writeln! (
out ,
2026-04-01 01:44:55 +00:00
" claw [--model MODEL] [--output-format text|json] prompt TEXT "
2026-04-01 01:14:44 +00:00
) ? ;
writeln! ( out , " Send one prompt and exit " ) ? ;
writeln! (
out ,
2026-04-01 01:44:55 +00:00
" claw [--model MODEL] [--output-format text|json] TEXT "
2026-04-01 01:14:44 +00:00
) ? ;
writeln! ( out , " Shorthand non-interactive prompt mode " ) ? ;
writeln! (
out ,
2026-04-02 07:15:03 +00:00
" claw --resume [SESSION.jsonl|session-id|latest] [/status] [/compact] [...] "
2026-04-01 01:14:44 +00:00
) ? ;
writeln! (
out ,
" Inspect or maintain a saved session without entering the REPL "
) ? ;
2026-04-02 07:44:39 +00:00
writeln! ( out , " claw help " ) ? ;
writeln! ( out , " Alias for --help " ) ? ;
writeln! ( out , " claw version " ) ? ;
writeln! ( out , " Alias for --version " ) ? ;
writeln! ( out , " claw status " ) ? ;
writeln! (
out ,
" Show the current local workspace status snapshot "
) ? ;
writeln! ( out , " claw sandbox " ) ? ;
writeln! ( out , " Show the current sandbox isolation snapshot " ) ? ;
2026-04-01 01:44:55 +00:00
writeln! ( out , " claw dump-manifests " ) ? ;
writeln! ( out , " claw bootstrap-plan " ) ? ;
2026-04-01 08:30:02 +00:00
writeln! ( out , " claw agents " ) ? ;
2026-04-02 10:04:40 +00:00
writeln! ( out , " claw mcp " ) ? ;
2026-04-01 08:30:02 +00:00
writeln! ( out , " claw skills " ) ? ;
2026-04-01 02:04:12 +00:00
writeln! ( out , " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD] " ) ? ;
2026-04-01 01:44:55 +00:00
writeln! ( out , " claw login " ) ? ;
writeln! ( out , " claw logout " ) ? ;
writeln! ( out , " claw init " ) ? ;
2026-04-01 01:14:44 +00:00
writeln! ( out ) ? ;
writeln! ( out , " Flags: " ) ? ;
writeln! (
out ,
" --model MODEL Override the active model "
) ? ;
writeln! (
out ,
" --output-format FORMAT Non-interactive output format: text or json "
) ? ;
writeln! (
out ,
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access "
) ? ;
2026-04-01 02:42:49 +00:00
writeln! (
out ,
" --dangerously-skip-permissions Skip all permission checks "
) ? ;
2026-04-01 01:14:44 +00:00
writeln! ( out , " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported) " ) ? ;
writeln! (
out ,
" --version, -V Print version and build information locally "
) ? ;
writeln! ( out ) ? ;
writeln! ( out , " Interactive slash commands: " ) ? ;
writeln! ( out , " {} " , render_slash_command_help ( ) ) ? ;
writeln! ( out ) ? ;
2026-03-31 20:01:48 +00:00
let resume_commands = resume_supported_slash_commands ( )
. into_iter ( )
. map ( | spec | match spec . argument_hint {
Some ( argument_hint ) = > format! ( " / {} {} " , spec . name , argument_hint ) ,
None = > format! ( " / {} " , spec . name ) ,
} )
. collect ::< Vec < _ > > ( )
. join ( " , " ) ;
2026-04-01 01:14:44 +00:00
writeln! ( out , " Resume-safe commands: {resume_commands} " ) ? ;
2026-04-02 07:15:03 +00:00
writeln! ( out ) ? ;
writeln! ( out , " Session shortcuts: " ) ? ;
writeln! (
out ,
" REPL turns auto-save to .claw/sessions/<session-id>.{PRIMARY_SESSION_EXTENSION} "
) ? ;
writeln! (
out ,
" Use `{LATEST_SESSION_REFERENCE}` with --resume, /resume, or /session switch to target the newest saved session "
) ? ;
writeln! (
out ,
" Use /session list in the REPL to browse managed sessions "
) ? ;
2026-04-01 01:14:44 +00:00
writeln! ( out , " Examples: " ) ? ;
2026-04-01 02:04:12 +00:00
writeln! ( out , " claw --model claude-opus \" summarize this repo \" " ) ? ;
2026-04-01 01:14:44 +00:00
writeln! (
out ,
2026-04-01 01:44:55 +00:00
" claw --output-format json prompt \" explain src/main.rs \" "
2026-04-01 01:14:44 +00:00
) ? ;
writeln! (
out ,
2026-04-01 01:44:55 +00:00
" claw --allowedTools read,glob \" summarize Cargo.toml \" "
2026-04-01 01:14:44 +00:00
) ? ;
2026-04-02 07:15:03 +00:00
writeln! ( out , " claw --resume {LATEST_SESSION_REFERENCE} " ) ? ;
2026-04-01 01:14:44 +00:00
writeln! (
out ,
2026-04-02 07:15:03 +00:00
" claw --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt "
2026-04-01 01:14:44 +00:00
) ? ;
2026-04-01 08:30:02 +00:00
writeln! ( out , " claw agents " ) ? ;
2026-04-02 10:04:40 +00:00
writeln! ( out , " claw mcp show my-server " ) ? ;
2026-04-01 08:30:02 +00:00
writeln! ( out , " claw /skills " ) ? ;
2026-04-01 01:44:55 +00:00
writeln! ( out , " claw login " ) ? ;
writeln! ( out , " claw init " ) ? ;
2026-04-01 01:14:44 +00:00
Ok ( ( ) )
}
fn print_help ( ) {
let _ = print_help_to ( & mut io ::stdout ( ) ) ;
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
#[ cfg(test) ]
mod tests {
2026-03-31 19:27:31 +00:00
use super ::{
2026-04-02 10:04:54 +00:00
build_runtime_plugin_state_with_loader , build_runtime_with_plugin_state ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
create_managed_session_handle , describe_tool_progress , filter_tool_specs ,
2026-04-02 18:10:32 +09:00
format_bughunter_report , format_commit_preflight_report , format_commit_skipped_report ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
format_compact_report , format_cost_report , format_internal_prompt_progress_line ,
2026-04-02 18:10:32 +09:00
format_issue_report , format_model_report , format_model_switch_report ,
format_permissions_report , format_permissions_switch_report , format_pr_report ,
format_resume_report , format_status_report , format_tool_call_start , format_tool_result ,
format_ultraplan_report , format_unknown_slash_command ,
2026-04-02 17:52:31 +09:00
format_unknown_slash_command_message , normalize_permission_mode , parse_args ,
parse_git_status_branch , parse_git_status_metadata_for , parse_git_workspace_summary ,
permission_policy , print_help_to , push_output_block , render_config_report ,
2026-04-04 02:18:52 +00:00
render_diff_report , render_diff_report_for , render_memory_report , render_repl_help ,
render_resume_usage , resolve_model_alias , resolve_session_reference , response_to_events ,
2026-04-02 18:14:09 +09:00
resume_supported_slash_commands , run_resume_command ,
slash_command_completion_candidates_with_sessions , status_context , validate_no_args ,
2026-04-03 14:31:25 +00:00
write_mcp_server_fixture , CliAction , CliOutputFormat , CliToolExecutor , GitWorkspaceSummary ,
InternalPromptProgressEvent , InternalPromptProgressState , LiveCli , SlashCommand ,
StatusUsage , DEFAULT_MODEL ,
2026-03-31 19:27:31 +00:00
} ;
2026-04-01 02:42:49 +00:00
use api ::{ MessageResponse , OutputContentBlock , Usage } ;
2026-04-02 10:04:54 +00:00
use plugins ::{
PluginManager , PluginManagerConfig , PluginTool , PluginToolDefinition , PluginToolPermission ,
} ;
2026-04-02 10:38:55 +09:00
use runtime ::{
2026-04-02 10:04:54 +00:00
AssistantEvent , ConfigLoader , ContentBlock , ConversationMessage , MessageRole ,
2026-04-03 14:31:25 +00:00
PermissionMode , Session , ToolExecutor ,
2026-04-02 10:38:55 +09:00
} ;
2026-04-01 02:42:49 +00:00
use serde_json ::json ;
2026-04-01 01:10:57 +00:00
use std ::fs ;
2026-03-31 19:57:38 +00:00
use std ::path ::{ Path , PathBuf } ;
2026-04-01 01:10:57 +00:00
use std ::process ::Command ;
use std ::sync ::{ Mutex , MutexGuard , OnceLock } ;
2026-04-02 10:38:55 +09:00
use std ::time ::{ Duration , SystemTime , UNIX_EPOCH } ;
2026-04-01 06:55:39 +00:00
use tools ::GlobalToolRegistry ;
2026-04-01 03:55:00 +00:00
2026-04-01 07:22:41 +00:00
fn registry_with_plugin_tool ( ) -> GlobalToolRegistry {
GlobalToolRegistry ::with_plugin_tools ( vec! [ PluginTool ::new (
" plugin-demo@external " ,
" plugin-demo " ,
PluginToolDefinition {
name : " plugin_echo " . to_string ( ) ,
description : Some ( " Echo plugin payload " . to_string ( ) ) ,
input_schema : json ! ( {
" type " : " object " ,
" properties " : {
" message " : { " type " : " string " }
} ,
" required " : [ " message " ] ,
" additionalProperties " : false
} ) ,
} ,
" echo " . to_string ( ) ,
Vec ::new ( ) ,
PluginToolPermission ::WorkspaceWrite ,
None ,
) ] )
. expect ( " plugin tool registry should build " )
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
2026-04-01 01:10:57 +00:00
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
}
2026-04-02 10:04:54 +00:00
fn write_plugin_fixture ( root : & Path , name : & str , include_hooks : bool , include_lifecycle : bool ) {
fs ::create_dir_all ( root . join ( " .claude-plugin " ) ) . expect ( " manifest dir " ) ;
if include_hooks {
fs ::create_dir_all ( root . join ( " hooks " ) ) . expect ( " hooks dir " ) ;
fs ::write (
root . join ( " hooks " ) . join ( " pre.sh " ) ,
" #!/bin/sh \n printf 'plugin pre hook' \n " ,
)
. expect ( " write hook " ) ;
}
if include_lifecycle {
fs ::create_dir_all ( root . join ( " lifecycle " ) ) . expect ( " lifecycle dir " ) ;
fs ::write (
root . join ( " lifecycle " ) . join ( " init.sh " ) ,
" #!/bin/sh \n printf 'init \\ n' >> lifecycle.log \n " ,
)
. expect ( " write init lifecycle " ) ;
fs ::write (
root . join ( " lifecycle " ) . join ( " shutdown.sh " ) ,
" #!/bin/sh \n printf 'shutdown \\ n' >> lifecycle.log \n " ,
)
. expect ( " write shutdown lifecycle " ) ;
}
let hooks = if include_hooks {
" , \n \" hooks \" : { \n \" PreToolUse \" : [ \" ./hooks/pre.sh \" ] \n } "
} else {
" "
} ;
let lifecycle = if include_lifecycle {
" , \n \" lifecycle \" : { \n \" Init \" : [ \" ./lifecycle/init.sh \" ], \n \" Shutdown \" : [ \" ./lifecycle/shutdown.sh \" ] \n } "
} else {
" "
} ;
fs ::write (
root . join ( " .claude-plugin " ) . join ( " plugin.json " ) ,
format! (
" {{ \n \" name \" : \" {name} \" , \n \" version \" : \" 1.0.0 \" , \n \" description \" : \" runtime plugin fixture \" {hooks}{lifecycle} \n }} "
) ,
)
. expect ( " write plugin manifest " ) ;
}
2026-03-31 18:39:39 +00:00
#[ test ]
fn defaults_to_repl_when_no_args ( ) {
2026-04-03 22:07:12 +09:00
let _guard = env_lock ( ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
2026-03-31 18:39:39 +00:00
assert_eq! (
parse_args ( & [ ] ) . expect ( " args should parse " ) ,
CliAction ::Repl {
model : DEFAULT_MODEL . to_string ( ) ,
2026-03-31 23:38:53 +00:00
allowed_tools : None ,
2026-04-01 02:42:49 +00:00
permission_mode : PermissionMode ::DangerFullAccess ,
2026-03-31 18:39:39 +00:00
}
) ;
}
2026-04-02 10:02:26 +00:00
#[ test ]
fn default_permission_mode_uses_project_config_when_env_is_unset ( ) {
let _guard = env_lock ( ) ;
let root = temp_dir ( ) ;
let cwd = root . join ( " project " ) ;
let config_home = root . join ( " config-home " ) ;
std ::fs ::create_dir_all ( cwd . join ( " .claw " ) ) . expect ( " project config dir should exist " ) ;
std ::fs ::create_dir_all ( & config_home ) . expect ( " config home should exist " ) ;
std ::fs ::write (
cwd . join ( " .claw " ) . join ( " settings.json " ) ,
r # "{"permissionMode":"acceptEdits"}"# ,
)
. expect ( " project config should write " ) ;
let original_config_home = std ::env ::var ( " CLAW_CONFIG_HOME " ) . ok ( ) ;
let original_permission_mode = std ::env ::var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) . ok ( ) ;
std ::env ::set_var ( " CLAW_CONFIG_HOME " , & config_home ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
let resolved = with_current_dir ( & cwd , super ::default_permission_mode ) ;
match original_config_home {
Some ( value ) = > std ::env ::set_var ( " CLAW_CONFIG_HOME " , value ) ,
None = > std ::env ::remove_var ( " CLAW_CONFIG_HOME " ) ,
}
match original_permission_mode {
Some ( value ) = > std ::env ::set_var ( " RUSTY_CLAUDE_PERMISSION_MODE " , value ) ,
None = > std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ,
}
std ::fs ::remove_dir_all ( root ) . expect ( " temp config root should clean up " ) ;
assert_eq! ( resolved , PermissionMode ::WorkspaceWrite ) ;
}
#[ test ]
fn env_permission_mode_overrides_project_config_default ( ) {
let _guard = env_lock ( ) ;
let root = temp_dir ( ) ;
let cwd = root . join ( " project " ) ;
let config_home = root . join ( " config-home " ) ;
std ::fs ::create_dir_all ( cwd . join ( " .claw " ) ) . expect ( " project config dir should exist " ) ;
std ::fs ::create_dir_all ( & config_home ) . expect ( " config home should exist " ) ;
std ::fs ::write (
cwd . join ( " .claw " ) . join ( " settings.json " ) ,
r # "{"permissionMode":"acceptEdits"}"# ,
)
. expect ( " project config should write " ) ;
let original_config_home = std ::env ::var ( " CLAW_CONFIG_HOME " ) . ok ( ) ;
let original_permission_mode = std ::env ::var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) . ok ( ) ;
std ::env ::set_var ( " CLAW_CONFIG_HOME " , & config_home ) ;
std ::env ::set_var ( " RUSTY_CLAUDE_PERMISSION_MODE " , " read-only " ) ;
let resolved = with_current_dir ( & cwd , super ::default_permission_mode ) ;
match original_config_home {
Some ( value ) = > std ::env ::set_var ( " CLAW_CONFIG_HOME " , value ) ,
None = > std ::env ::remove_var ( " CLAW_CONFIG_HOME " ) ,
}
match original_permission_mode {
Some ( value ) = > std ::env ::set_var ( " RUSTY_CLAUDE_PERMISSION_MODE " , value ) ,
None = > std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ,
}
std ::fs ::remove_dir_all ( root ) . expect ( " temp config root should clean up " ) ;
assert_eq! ( resolved , PermissionMode ::ReadOnly ) ;
}
2026-03-31 18:39:39 +00:00
#[ test ]
fn parses_prompt_subcommand ( ) {
2026-04-03 22:07:12 +09:00
let _guard = env_lock ( ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
2026-03-31 18:39:39 +00:00
let args = vec! [
" prompt " . to_string ( ) ,
" hello " . to_string ( ) ,
" world " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Prompt {
prompt : " hello world " . to_string ( ) ,
model : DEFAULT_MODEL . to_string ( ) ,
2026-03-31 22:49:50 +00:00
output_format : CliOutputFormat ::Text ,
2026-03-31 23:38:53 +00:00
allowed_tools : None ,
2026-04-01 02:42:49 +00:00
permission_mode : PermissionMode ::DangerFullAccess ,
2026-03-31 22:49:50 +00:00
}
) ;
}
#[ test ]
fn parses_bare_prompt_and_json_output_flag ( ) {
2026-04-03 22:07:12 +09:00
let _guard = env_lock ( ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
2026-03-31 22:49:50 +00:00
let args = vec! [
" --output-format=json " . to_string ( ) ,
" --model " . to_string ( ) ,
" claude-opus " . to_string ( ) ,
" explain " . to_string ( ) ,
" this " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Prompt {
prompt : " explain this " . to_string ( ) ,
model : " claude-opus " . to_string ( ) ,
output_format : CliOutputFormat ::Json ,
2026-03-31 23:38:53 +00:00
allowed_tools : None ,
2026-04-01 02:42:49 +00:00
permission_mode : PermissionMode ::DangerFullAccess ,
2026-03-31 18:39:39 +00:00
}
) ;
}
2026-04-01 02:04:12 +00:00
#[ test ]
fn resolves_model_aliases_in_args ( ) {
2026-04-03 22:07:12 +09:00
let _guard = env_lock ( ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
2026-04-01 02:04:12 +00:00
let args = vec! [
" --model " . to_string ( ) ,
" opus " . to_string ( ) ,
" explain " . to_string ( ) ,
" this " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Prompt {
prompt : " explain this " . to_string ( ) ,
model : " claude-opus-4-6 " . to_string ( ) ,
output_format : CliOutputFormat ::Text ,
allowed_tools : None ,
2026-04-01 02:42:49 +00:00
permission_mode : PermissionMode ::DangerFullAccess ,
2026-04-01 02:04:12 +00:00
}
) ;
}
#[ test ]
fn resolves_known_model_aliases ( ) {
assert_eq! ( resolve_model_alias ( " opus " ) , " claude-opus-4-6 " ) ;
assert_eq! ( resolve_model_alias ( " sonnet " ) , " claude-sonnet-4-6 " ) ;
2026-04-01 02:42:49 +00:00
assert_eq! ( resolve_model_alias ( " haiku " ) , " claude-haiku-4-5-20251213 " ) ;
2026-04-01 02:04:12 +00:00
assert_eq! ( resolve_model_alias ( " claude-opus " ) , " claude-opus " ) ;
}
2026-03-31 23:38:53 +00:00
#[ test ]
fn parses_version_flags_without_initializing_prompt_mode ( ) {
assert_eq! (
parse_args ( & [ " --version " . to_string ( ) ] ) . expect ( " args should parse " ) ,
CliAction ::Version
) ;
assert_eq! (
parse_args ( & [ " -V " . to_string ( ) ] ) . expect ( " args should parse " ) ,
CliAction ::Version
) ;
}
2026-04-01 00:06:15 +00:00
#[ test ]
fn parses_permission_mode_flag ( ) {
let args = vec! [ " --permission-mode=read-only " . to_string ( ) ] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Repl {
model : DEFAULT_MODEL . to_string ( ) ,
allowed_tools : None ,
permission_mode : PermissionMode ::ReadOnly ,
}
) ;
}
2026-03-31 23:38:53 +00:00
#[ test ]
fn parses_allowed_tools_flags_with_aliases_and_lists ( ) {
2026-04-03 22:07:12 +09:00
let _guard = env_lock ( ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
2026-03-31 23:38:53 +00:00
let args = vec! [
" --allowedTools " . to_string ( ) ,
" read,glob " . to_string ( ) ,
" --allowed-tools=write_file " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::Repl {
model : DEFAULT_MODEL . to_string ( ) ,
allowed_tools : Some (
[ " glob_search " , " read_file " , " write_file " ]
. into_iter ( )
. map ( str ::to_string )
. collect ( )
) ,
2026-04-01 02:42:49 +00:00
permission_mode : PermissionMode ::DangerFullAccess ,
2026-03-31 23:38:53 +00:00
}
) ;
}
#[ test ]
fn rejects_unknown_allowed_tools ( ) {
let error = parse_args ( & [ " --allowedTools " . to_string ( ) , " teleport " . to_string ( ) ] )
. expect_err ( " tool should be rejected " ) ;
assert! ( error . contains ( " unsupported tool in --allowedTools: teleport " ) ) ;
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
#[ test ]
fn parses_system_prompt_options ( ) {
let args = vec! [
" system-prompt " . to_string ( ) ,
" --cwd " . to_string ( ) ,
" /tmp/project " . to_string ( ) ,
" --date " . to_string ( ) ,
" 2026-04-01 " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::PrintSystemPrompt {
cwd : PathBuf ::from ( " /tmp/project " ) ,
date : " 2026-04-01 " . to_string ( ) ,
}
) ;
}
2026-03-31 23:38:05 +00:00
#[ test ]
fn parses_login_and_logout_subcommands ( ) {
assert_eq! (
parse_args ( & [ " login " . to_string ( ) ] ) . expect ( " login should parse " ) ,
CliAction ::Login
) ;
assert_eq! (
parse_args ( & [ " logout " . to_string ( ) ] ) . expect ( " logout should parse " ) ,
CliAction ::Logout
) ;
2026-04-01 01:14:44 +00:00
assert_eq! (
parse_args ( & [ " init " . to_string ( ) ] ) . expect ( " init should parse " ) ,
CliAction ::Init
) ;
2026-04-01 08:30:02 +00:00
assert_eq! (
parse_args ( & [ " agents " . to_string ( ) ] ) . expect ( " agents should parse " ) ,
CliAction ::Agents { args : None }
) ;
2026-04-02 10:04:40 +00:00
assert_eq! (
parse_args ( & [ " mcp " . to_string ( ) ] ) . expect ( " mcp should parse " ) ,
CliAction ::Mcp { args : None }
) ;
2026-04-01 08:30:02 +00:00
assert_eq! (
parse_args ( & [ " skills " . to_string ( ) ] ) . expect ( " skills should parse " ) ,
CliAction ::Skills { args : None }
) ;
assert_eq! (
parse_args ( & [ " agents " . to_string ( ) , " --help " . to_string ( ) ] )
. expect ( " agents help should parse " ) ,
CliAction ::Agents {
args : Some ( " --help " . to_string ( ) )
}
) ;
}
2026-04-02 07:44:39 +00:00
#[ test ]
fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode ( ) {
2026-04-03 22:07:12 +09:00
let _guard = env_lock ( ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
2026-04-02 07:44:39 +00:00
assert_eq! (
parse_args ( & [ " help " . to_string ( ) ] ) . expect ( " help should parse " ) ,
CliAction ::Help
) ;
assert_eq! (
parse_args ( & [ " version " . to_string ( ) ] ) . expect ( " version should parse " ) ,
CliAction ::Version
) ;
assert_eq! (
parse_args ( & [ " status " . to_string ( ) ] ) . expect ( " status should parse " ) ,
CliAction ::Status {
model : DEFAULT_MODEL . to_string ( ) ,
permission_mode : PermissionMode ::DangerFullAccess ,
}
) ;
assert_eq! (
parse_args ( & [ " sandbox " . to_string ( ) ] ) . expect ( " sandbox should parse " ) ,
CliAction ::Sandbox
) ;
}
#[ test ]
fn single_word_slash_command_names_return_guidance_instead_of_hitting_prompt_mode ( ) {
let error = parse_args ( & [ " cost " . to_string ( ) ] ) . expect_err ( " cost should return guidance " ) ;
assert! ( error . contains ( " slash command " ) ) ;
assert! ( error . contains ( " /cost " ) ) ;
}
#[ test ]
fn multi_word_prompt_still_uses_shorthand_prompt_mode ( ) {
2026-04-03 22:07:12 +09:00
let _guard = env_lock ( ) ;
std ::env ::remove_var ( " RUSTY_CLAUDE_PERMISSION_MODE " ) ;
2026-04-02 07:44:39 +00:00
assert_eq! (
parse_args ( & [ " help " . to_string ( ) , " me " . to_string ( ) , " debug " . to_string ( ) ] )
. expect ( " prompt shorthand should still work " ) ,
CliAction ::Prompt {
prompt : " help me debug " . to_string ( ) ,
model : DEFAULT_MODEL . to_string ( ) ,
output_format : CliOutputFormat ::Text ,
allowed_tools : None ,
permission_mode : PermissionMode ::DangerFullAccess ,
}
) ;
}
2026-04-01 08:30:02 +00:00
#[ test ]
2026-04-02 10:04:40 +00:00
fn parses_direct_agents_mcp_and_skills_slash_commands ( ) {
2026-04-01 08:30:02 +00:00
assert_eq! (
parse_args ( & [ " /agents " . to_string ( ) ] ) . expect ( " /agents should parse " ) ,
CliAction ::Agents { args : None }
) ;
2026-04-02 10:04:40 +00:00
assert_eq! (
parse_args ( & [ " /mcp " . to_string ( ) , " show " . to_string ( ) , " demo " . to_string ( ) ] )
. expect ( " /mcp show demo should parse " ) ,
CliAction ::Mcp {
args : Some ( " show demo " . to_string ( ) )
}
) ;
2026-04-01 08:30:02 +00:00
assert_eq! (
parse_args ( & [ " /skills " . to_string ( ) ] ) . expect ( " /skills should parse " ) ,
CliAction ::Skills { args : None }
) ;
assert_eq! (
parse_args ( & [ " /skills " . to_string ( ) , " help " . to_string ( ) ] )
. expect ( " /skills help should parse " ) ,
CliAction ::Skills {
args : Some ( " help " . to_string ( ) )
}
) ;
2026-04-02 10:03:22 +00:00
assert_eq! (
parse_args ( & [
" /skills " . to_string ( ) ,
" install " . to_string ( ) ,
" ./fixtures/help-skill " . to_string ( ) ,
] )
. expect ( " /skills install should parse " ) ,
CliAction ::Skills {
args : Some ( " install ./fixtures/help-skill " . to_string ( ) )
2026-04-01 08:30:02 +00:00
}
) ;
let error = parse_args ( & [ " /status " . to_string ( ) ] )
. expect_err ( " /status should remain REPL-only when invoked directly " ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
assert! ( error . contains ( " interactive-only " ) ) ;
assert! ( error . contains ( " claw --resume SESSION.jsonl /status " ) ) ;
}
2026-04-02 18:24:47 +09:00
#[ test ]
fn direct_slash_commands_surface_shared_validation_errors ( ) {
let compact_error = parse_args ( & [ " /compact " . to_string ( ) , " now " . to_string ( ) ] )
. expect_err ( " invalid /compact shape should be rejected " ) ;
assert! ( compact_error . contains ( " Unexpected arguments for /compact. " ) ) ;
assert! ( compact_error . contains ( " Usage /compact " ) ) ;
let plugins_error = parse_args ( & [
" /plugins " . to_string ( ) ,
" list " . to_string ( ) ,
" extra " . to_string ( ) ,
] )
. expect_err ( " invalid /plugins list shape should be rejected " ) ;
assert! ( plugins_error . contains ( " Usage: /plugin list " ) ) ;
assert! ( plugins_error . contains ( " Aliases /plugins, /marketplace " ) ) ;
}
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
#[ test ]
fn formats_unknown_slash_command_with_suggestions ( ) {
feat: add 40 slash commands — command surface 67/141
Port 40 missing user-facing slash commands from upstream parity audit:
Session: /doctor, /login, /logout, /usage, /stats, /rename, /privacy-settings
Workspace: /branch, /add-dir, /files, /hooks, /release-notes
Discovery: /context, /tasks, /doctor, /ide, /desktop
Analysis: /review, /security-review, /advisor, /insights
Appearance: /theme, /vim, /voice, /color, /effort, /fast, /brief,
/output-style, /keybindings, /stickers
Communication: /copy, /share, /feedback, /summary, /tag, /thinkback,
/plan, /exit, /upgrade, /rewind
All commands have full SlashCommandSpec, enum variant, parse arm,
and stub handler. Category system expanded with two new categories.
Tests updated for new counts (67 specs, 39 resume-supported).
fmt/clippy/tests all green.
2026-04-03 08:09:14 +09:00
let report = format_unknown_slash_command_message ( " statsu " ) ;
assert! ( report . contains ( " unknown slash command: /statsu " ) ) ;
assert! ( report . contains ( " Did you mean " ) ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
assert! ( report . contains ( " Use /help " ) ) ;
2026-03-31 23:38:05 +00:00
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
#[ test ]
fn parses_resume_flag_with_slash_command ( ) {
let args = vec! [
" --resume " . to_string ( ) ,
2026-04-01 06:15:14 +00:00
" session.jsonl " . to_string ( ) ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
" /compact " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::ResumeSession {
2026-04-01 06:15:14 +00:00
session_path : PathBuf ::from ( " session.jsonl " ) ,
2026-03-31 20:00:13 +00:00
commands : vec ! [ " /compact " . to_string ( ) ] ,
}
) ;
}
2026-04-02 07:15:03 +00:00
#[ test ]
fn parses_resume_flag_without_path_as_latest_session ( ) {
assert_eq! (
parse_args ( & [ " --resume " . to_string ( ) ] ) . expect ( " args should parse " ) ,
CliAction ::ResumeSession {
session_path : PathBuf ::from ( " latest " ) ,
commands : vec ! [ ] ,
}
) ;
assert_eq! (
parse_args ( & [ " --resume " . to_string ( ) , " /status " . to_string ( ) ] )
. expect ( " resume shortcut should parse " ) ,
CliAction ::ResumeSession {
session_path : PathBuf ::from ( " latest " ) ,
commands : vec ! [ " /status " . to_string ( ) ] ,
}
) ;
}
2026-03-31 20:00:13 +00:00
#[ test ]
fn parses_resume_flag_with_multiple_slash_commands ( ) {
let args = vec! [
" --resume " . to_string ( ) ,
2026-04-01 06:15:14 +00:00
" session.jsonl " . to_string ( ) ,
2026-03-31 20:00:13 +00:00
" /status " . to_string ( ) ,
" /compact " . to_string ( ) ,
" /cost " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::ResumeSession {
2026-04-01 06:15:14 +00:00
session_path : PathBuf ::from ( " session.jsonl " ) ,
2026-03-31 20:00:13 +00:00
commands : vec ! [
" /status " . to_string ( ) ,
" /compact " . to_string ( ) ,
" /cost " . to_string ( ) ,
] ,
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
) ;
}
2026-03-31 18:39:39 +00:00
2026-04-02 07:15:03 +00:00
#[ test ]
fn rejects_unknown_options_with_helpful_guidance ( ) {
let error = parse_args ( & [ " --resum " . to_string ( ) ] ) . expect_err ( " unknown option should fail " ) ;
assert! ( error . contains ( " unknown option: --resum " ) ) ;
assert! ( error . contains ( " Did you mean --resume? " ) ) ;
assert! ( error . contains ( " claw --help " ) ) ;
}
2026-04-02 07:37:25 +00:00
#[ test ]
fn parses_resume_flag_with_slash_command_arguments ( ) {
let args = vec! [
" --resume " . to_string ( ) ,
" session.jsonl " . to_string ( ) ,
" /export " . to_string ( ) ,
" notes.txt " . to_string ( ) ,
" /clear " . to_string ( ) ,
" --confirm " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::ResumeSession {
session_path : PathBuf ::from ( " session.jsonl " ) ,
commands : vec ! [
" /export notes.txt " . to_string ( ) ,
2026-04-02 08:35:56 +00:00
" /clear --confirm " . to_string ( ) ,
2026-04-02 07:37:25 +00:00
] ,
}
) ;
}
#[ test ]
fn parses_resume_flag_with_absolute_export_path ( ) {
let args = vec! [
" --resume " . to_string ( ) ,
" session.jsonl " . to_string ( ) ,
" /export " . to_string ( ) ,
" /tmp/notes.txt " . to_string ( ) ,
" /status " . to_string ( ) ,
] ;
assert_eq! (
parse_args ( & args ) . expect ( " args should parse " ) ,
CliAction ::ResumeSession {
session_path : PathBuf ::from ( " session.jsonl " ) ,
commands : vec ! [ " /export /tmp/notes.txt " . to_string ( ) , " /status " . to_string ( ) ] ,
}
) ;
}
2026-03-31 23:38:53 +00:00
#[ test ]
fn filtered_tool_specs_respect_allowlist ( ) {
let allowed = [ " read_file " , " grep_search " ]
. into_iter ( )
. map ( str ::to_string )
. collect ( ) ;
2026-04-01 04:30:28 +00:00
let filtered = filter_tool_specs ( & GlobalToolRegistry ::builtin ( ) , Some ( & allowed ) ) ;
2026-03-31 23:38:53 +00:00
let names = filtered
. into_iter ( )
. map ( | spec | spec . name )
. collect ::< Vec < _ > > ( ) ;
assert_eq! ( names , vec! [ " read_file " , " grep_search " ] ) ;
}
2026-04-01 07:22:41 +00:00
#[ test ]
fn filtered_tool_specs_include_plugin_tools ( ) {
let filtered = filter_tool_specs ( & registry_with_plugin_tool ( ) , None ) ;
let names = filtered
. into_iter ( )
. map ( | definition | definition . name )
. collect ::< Vec < _ > > ( ) ;
assert! ( names . contains ( & " bash " . to_string ( ) ) ) ;
assert! ( names . contains ( & " plugin_echo " . to_string ( ) ) ) ;
}
#[ test ]
fn permission_policy_uses_plugin_tool_permissions ( ) {
2026-04-02 11:31:53 +09:00
let feature_config = runtime ::RuntimeFeatureConfig ::default ( ) ;
let policy = permission_policy (
PermissionMode ::ReadOnly ,
& feature_config ,
& registry_with_plugin_tool ( ) ,
2026-04-02 18:04:55 +09:00
)
. expect ( " permission policy should build " ) ;
2026-04-01 07:22:41 +00:00
let required = policy . required_mode_for ( " plugin_echo " ) ;
assert_eq! ( required , PermissionMode ::WorkspaceWrite ) ;
}
2026-03-31 21:03:49 +00:00
#[ test ]
fn shared_help_uses_resume_annotation_copy ( ) {
let help = commands ::render_slash_command_help ( ) ;
assert! ( help . contains ( " Slash commands " ) ) ;
2026-04-01 06:15:14 +00:00
assert! ( help . contains ( " works with --resume SESSION.jsonl " ) ) ;
2026-03-31 21:03:49 +00:00
}
2026-03-31 19:23:05 +00:00
#[ test ]
fn repl_help_includes_shared_commands_and_exit ( ) {
let help = render_repl_help ( ) ;
2026-03-31 21:03:49 +00:00
assert! ( help . contains ( " REPL " ) ) ;
2026-03-31 19:23:05 +00:00
assert! ( help . contains ( " /help " ) ) ;
2026-04-02 07:19:14 +00:00
assert! ( help . contains ( " Complete commands, modes, and recent sessions " ) ) ;
2026-03-31 19:23:05 +00:00
assert! ( help . contains ( " /status " ) ) ;
2026-04-01 01:14:38 +00:00
assert! ( help . contains ( " /sandbox " ) ) ;
2026-03-31 19:23:05 +00:00
assert! ( help . contains ( " /model [model] " ) ) ;
2026-03-31 19:27:31 +00:00
assert! ( help . contains ( " /permissions [read-only|workspace-write|danger-full-access] " ) ) ;
2026-03-31 20:42:50 +00:00
assert! ( help . contains ( " /clear [--confirm] " ) ) ;
2026-03-31 19:27:31 +00:00
assert! ( help . contains ( " /cost " ) ) ;
2026-03-31 19:45:25 +00:00
assert! ( help . contains ( " /resume <session-path> " ) ) ;
2026-04-01 18:57:50 +09:00
assert! ( help . contains ( " /config [env|hooks|model|plugins] " ) ) ;
2026-04-02 10:04:40 +00:00
assert! ( help . contains ( " /mcp [list|show <server>|help] " ) ) ;
2026-03-31 19:54:09 +00:00
assert! ( help . contains ( " /memory " ) ) ;
2026-03-31 19:57:38 +00:00
assert! ( help . contains ( " /init " ) ) ;
2026-03-31 22:49:50 +00:00
assert! ( help . contains ( " /diff " ) ) ;
assert! ( help . contains ( " /version " ) ) ;
assert! ( help . contains ( " /export [file] " ) ) ;
2026-04-01 06:15:14 +00:00
assert! ( help . contains ( " /session [list|switch <session-id>|fork [branch-name]] " ) ) ;
2026-04-01 04:30:28 +00:00
assert! ( help . contains (
2026-04-01 08:19:25 +00:00
" /plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>] "
2026-04-01 04:30:28 +00:00
) ) ;
2026-04-01 08:19:25 +00:00
assert! ( help . contains ( " aliases: /plugins, /marketplace " ) ) ;
assert! ( help . contains ( " /agents " ) ) ;
assert! ( help . contains ( " /skills " ) ) ;
2026-03-31 19:23:05 +00:00
assert! ( help . contains ( " /exit " ) ) ;
2026-04-02 07:15:03 +00:00
assert! ( help . contains ( " Auto-save .claw/sessions/<session-id>.jsonl " ) ) ;
assert! ( help . contains ( " Resume latest /resume latest " ) ) ;
2026-03-31 19:23:05 +00:00
}
2026-04-02 07:19:14 +00:00
#[ test ]
fn completion_candidates_include_workflow_shortcuts_and_dynamic_sessions ( ) {
let completions = slash_command_completion_candidates_with_sessions (
" sonnet " ,
Some ( " session-current " ) ,
vec! [ " session-old " . to_string ( ) ] ,
) ;
assert! ( completions . contains ( & " /model claude-sonnet-4-6 " . to_string ( ) ) ) ;
assert! ( completions . contains ( & " /permissions workspace-write " . to_string ( ) ) ) ;
assert! ( completions . contains ( & " /session list " . to_string ( ) ) ) ;
assert! ( completions . contains ( & " /session switch session-current " . to_string ( ) ) ) ;
assert! ( completions . contains ( & " /resume session-old " . to_string ( ) ) ) ;
2026-04-02 10:04:40 +00:00
assert! ( completions . contains ( & " /mcp list " . to_string ( ) ) ) ;
2026-04-02 07:19:14 +00:00
assert! ( completions . contains ( & " /ultraplan " . to_string ( ) ) ) ;
}
#[ test ]
fn startup_banner_mentions_workflow_completions ( ) {
let _guard = env_lock ( ) ;
2026-04-03 07:04:30 +09:00
// Inject dummy credentials so LiveCli can construct without real Anthropic key
std ::env ::set_var ( " ANTHROPIC_API_KEY " , " test-dummy-key-for-banner-test " ) ;
2026-04-02 07:19:14 +00:00
let root = temp_dir ( ) ;
fs ::create_dir_all ( & root ) . expect ( " root dir " ) ;
let banner = with_current_dir ( & root , | | {
LiveCli ::new (
" claude-sonnet-4-6 " . to_string ( ) ,
true ,
None ,
PermissionMode ::DangerFullAccess ,
)
. expect ( " cli should initialize " )
. startup_banner ( )
} ) ;
assert! ( banner . contains ( " Tab " ) ) ;
assert! ( banner . contains ( " workflow completions " ) ) ;
fs ::remove_dir_all ( root ) . expect ( " cleanup temp dir " ) ;
2026-04-03 07:04:30 +09:00
std ::env ::remove_var ( " ANTHROPIC_API_KEY " ) ;
2026-04-02 07:19:14 +00:00
}
2026-03-31 20:01:48 +00:00
#[ test ]
fn resume_supported_command_list_matches_expected_surface ( ) {
let names = resume_supported_slash_commands ( )
. into_iter ( )
. map ( | spec | spec . name )
. collect ::< Vec < _ > > ( ) ;
feat(commands): expand slash command surface 67 → 135 specs
Add 68 new slash command specs covering:
- Approval flow: approve/deny
- Editing: undo, retry, paste, image, screenshot
- Code ops: test, lint, build, run, fix, refactor, explain, docs, perf
- Git: git, stash, blame, log
- LSP: symbols, references, definition, hover, diagnostics, autofix
- Navigation: focus/unfocus, web, map, search, workspace
- Model: max-tokens, temperature, system-prompt, tool-details
- Session: history, tokens, cache, pin/unpin, bookmarks, format
- Infra: cron, team, parallel, multi, macro, alias
- Config: api-key, language, profile, telemetry, env, project
- Other: providers, notifications, changelog, templates, benchmark, migrate, reset
Update tests: flexible assertions for expanded command surface
2026-04-03 19:52:40 +09:00
// Now with 135+ slash commands, verify minimum resume support
2026-04-04 00:42:43 +09:00
assert! (
names . len ( ) > = 39 ,
" expected at least 39 resume-supported commands, got {} " ,
names . len ( )
) ;
feat(commands): expand slash command surface 67 → 135 specs
Add 68 new slash command specs covering:
- Approval flow: approve/deny
- Editing: undo, retry, paste, image, screenshot
- Code ops: test, lint, build, run, fix, refactor, explain, docs, perf
- Git: git, stash, blame, log
- LSP: symbols, references, definition, hover, diagnostics, autofix
- Navigation: focus/unfocus, web, map, search, workspace
- Model: max-tokens, temperature, system-prompt, tool-details
- Session: history, tokens, cache, pin/unpin, bookmarks, format
- Infra: cron, team, parallel, multi, macro, alias
- Config: api-key, language, profile, telemetry, env, project
- Other: providers, notifications, changelog, templates, benchmark, migrate, reset
Update tests: flexible assertions for expanded command surface
2026-04-03 19:52:40 +09:00
// Verify key resume commands still exist
assert! ( names . contains ( & " help " ) ) ;
assert! ( names . contains ( & " status " ) ) ;
assert! ( names . contains ( & " compact " ) ) ;
2026-03-31 20:01:48 +00:00
}
2026-03-31 21:04:42 +00:00
#[ test ]
fn resume_report_uses_sectioned_layout ( ) {
2026-04-01 06:15:14 +00:00
let report = format_resume_report ( " session.jsonl " , 14 , 6 ) ;
2026-03-31 21:04:42 +00:00
assert! ( report . contains ( " Session resumed " ) ) ;
2026-04-01 06:15:14 +00:00
assert! ( report . contains ( " Session file session.jsonl " ) ) ;
2026-03-31 21:04:42 +00:00
assert! ( report . contains ( " Messages 14 " ) ) ;
assert! ( report . contains ( " Turns 6 " ) ) ;
}
2026-03-31 21:15:37 +00:00
#[ test ]
fn compact_report_uses_structured_output ( ) {
let compacted = format_compact_report ( 8 , 5 , false ) ;
assert! ( compacted . contains ( " Compact " ) ) ;
assert! ( compacted . contains ( " Result compacted " ) ) ;
assert! ( compacted . contains ( " Messages removed 8 " ) ) ;
let skipped = format_compact_report ( 0 , 3 , true ) ;
assert! ( skipped . contains ( " Result skipped " ) ) ;
}
2026-03-31 21:02:24 +00:00
#[ test ]
fn cost_report_uses_sectioned_layout ( ) {
let report = format_cost_report ( runtime ::TokenUsage {
input_tokens : 20 ,
output_tokens : 8 ,
cache_creation_input_tokens : 3 ,
cache_read_input_tokens : 1 ,
} ) ;
assert! ( report . contains ( " Cost " ) ) ;
assert! ( report . contains ( " Input tokens 20 " ) ) ;
assert! ( report . contains ( " Output tokens 8 " ) ) ;
assert! ( report . contains ( " Cache create 3 " ) ) ;
assert! ( report . contains ( " Cache read 1 " ) ) ;
assert! ( report . contains ( " Total tokens 32 " ) ) ;
}
2026-03-31 21:01:21 +00:00
#[ test ]
fn permissions_report_uses_sectioned_layout ( ) {
let report = format_permissions_report ( " workspace-write " ) ;
assert! ( report . contains ( " Permissions " ) ) ;
2026-03-31 22:19:58 +00:00
assert! ( report . contains ( " Active mode workspace-write " ) ) ;
assert! ( report . contains ( " Modes " ) ) ;
assert! ( report . contains ( " read-only ○ available Read/search tools only " ) ) ;
assert! ( report . contains ( " workspace-write ● current Edit files inside the workspace " ) ) ;
assert! ( report . contains ( " danger-full-access ○ available Unrestricted tool access " ) ) ;
2026-03-31 21:01:21 +00:00
}
#[ test ]
fn permissions_switch_report_is_structured ( ) {
let report = format_permissions_switch_report ( " read-only " , " workspace-write " ) ;
assert! ( report . contains ( " Permissions updated " ) ) ;
2026-03-31 22:19:58 +00:00
assert! ( report . contains ( " Result mode switched " ) ) ;
assert! ( report . contains ( " Previous mode read-only " ) ) ;
assert! ( report . contains ( " Active mode workspace-write " ) ) ;
assert! ( report . contains ( " Applies to subsequent tool calls " ) ) ;
2026-03-31 21:01:21 +00:00
}
2026-03-31 21:13:27 +00:00
#[ test ]
2026-04-01 01:14:44 +00:00
fn init_help_mentions_direct_subcommand ( ) {
let mut help = Vec ::new ( ) ;
print_help_to ( & mut help ) . expect ( " help should render " ) ;
let help = String ::from_utf8 ( help ) . expect ( " help should be utf8 " ) ;
2026-04-02 07:44:39 +00:00
assert! ( help . contains ( " claw help " ) ) ;
assert! ( help . contains ( " claw version " ) ) ;
assert! ( help . contains ( " claw status " ) ) ;
assert! ( help . contains ( " claw sandbox " ) ) ;
2026-04-01 01:44:55 +00:00
assert! ( help . contains ( " claw init " ) ) ;
2026-04-01 08:30:02 +00:00
assert! ( help . contains ( " claw agents " ) ) ;
2026-04-02 10:04:40 +00:00
assert! ( help . contains ( " claw mcp " ) ) ;
2026-04-01 08:30:02 +00:00
assert! ( help . contains ( " claw skills " ) ) ;
assert! ( help . contains ( " claw /skills " ) ) ;
2026-03-31 21:13:27 +00:00
}
2026-03-31 21:01:21 +00:00
2026-03-31 20:43:56 +00:00
#[ test ]
fn model_report_uses_sectioned_layout ( ) {
let report = format_model_report ( " claude-sonnet " , 12 , 4 ) ;
assert! ( report . contains ( " Model " ) ) ;
assert! ( report . contains ( " Current model claude-sonnet " ) ) ;
assert! ( report . contains ( " Session messages 12 " ) ) ;
assert! ( report . contains ( " Switch models with /model <name> " ) ) ;
}
#[ test ]
fn model_switch_report_preserves_context_summary ( ) {
let report = format_model_switch_report ( " claude-sonnet " , " claude-opus " , 9 ) ;
assert! ( report . contains ( " Model updated " ) ) ;
assert! ( report . contains ( " Previous claude-sonnet " ) ) ;
assert! ( report . contains ( " Current claude-opus " ) ) ;
assert! ( report . contains ( " Preserved msgs 9 " ) ) ;
}
2026-03-31 19:23:05 +00:00
#[ test ]
fn status_line_reports_model_and_token_totals ( ) {
2026-03-31 20:22:59 +00:00
let status = format_status_report (
2026-03-31 19:23:05 +00:00
" claude-sonnet " ,
2026-03-31 20:22:59 +00:00
StatusUsage {
message_count : 7 ,
turns : 3 ,
latest : runtime ::TokenUsage {
input_tokens : 5 ,
output_tokens : 4 ,
cache_creation_input_tokens : 1 ,
cache_read_input_tokens : 0 ,
} ,
cumulative : runtime ::TokenUsage {
input_tokens : 20 ,
output_tokens : 8 ,
cache_creation_input_tokens : 2 ,
cache_read_input_tokens : 1 ,
} ,
estimated_tokens : 128 ,
2026-03-31 19:23:05 +00:00
} ,
" workspace-write " ,
2026-03-31 20:22:59 +00:00
& super ::StatusContext {
cwd : PathBuf ::from ( " /tmp/project " ) ,
2026-04-01 06:15:14 +00:00
session_path : Some ( PathBuf ::from ( " session.jsonl " ) ) ,
2026-03-31 20:22:59 +00:00
loaded_config_files : 2 ,
discovered_config_files : 3 ,
memory_file_count : 4 ,
2026-03-31 21:06:51 +00:00
project_root : Some ( PathBuf ::from ( " /tmp " ) ) ,
git_branch : Some ( " main " . to_string ( ) ) ,
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
git_summary : GitWorkspaceSummary {
changed_files : 3 ,
staged_files : 1 ,
unstaged_files : 1 ,
untracked_files : 1 ,
conflicted_files : 0 ,
} ,
2026-04-01 01:14:38 +00:00
sandbox_status : runtime ::SandboxStatus ::default ( ) ,
2026-03-31 20:22:59 +00:00
} ,
2026-03-31 19:23:05 +00:00
) ;
2026-03-31 20:41:39 +00:00
assert! ( status . contains ( " Status " ) ) ;
assert! ( status . contains ( " Model claude-sonnet " ) ) ;
assert! ( status . contains ( " Permission mode workspace-write " ) ) ;
assert! ( status . contains ( " Messages 7 " ) ) ;
assert! ( status . contains ( " Latest total 10 " ) ) ;
assert! ( status . contains ( " Cumulative total 31 " ) ) ;
assert! ( status . contains ( " Cwd /tmp/project " ) ) ;
2026-03-31 21:06:51 +00:00
assert! ( status . contains ( " Project root /tmp " ) ) ;
assert! ( status . contains ( " Git branch main " ) ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
assert! (
status . contains ( " Git state dirty · 3 files · 1 staged, 1 unstaged, 1 untracked " )
) ;
assert! ( status . contains ( " Changed files 3 " ) ) ;
assert! ( status . contains ( " Staged 1 " ) ) ;
assert! ( status . contains ( " Unstaged 1 " ) ) ;
assert! ( status . contains ( " Untracked 1 " ) ) ;
2026-04-01 06:15:14 +00:00
assert! ( status . contains ( " Session session.jsonl " ) ) ;
2026-03-31 20:41:39 +00:00
assert! ( status . contains ( " Config files loaded 2/3 " ) ) ;
assert! ( status . contains ( " Memory files 4 " ) ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
assert! ( status . contains ( " Suggested flow /status → /diff → /commit " ) ) ;
}
#[ test ]
fn commit_reports_surface_workspace_context ( ) {
let summary = GitWorkspaceSummary {
changed_files : 2 ,
staged_files : 1 ,
unstaged_files : 1 ,
untracked_files : 0 ,
conflicted_files : 0 ,
} ;
let preflight = format_commit_preflight_report ( Some ( " feature/ux " ) , summary ) ;
2026-04-02 18:10:32 +09:00
assert! ( preflight . contains ( " Result ready " ) ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
assert! ( preflight . contains ( " Branch feature/ux " ) ) ;
assert! ( preflight . contains ( " Workspace dirty · 2 files · 1 staged, 1 unstaged " ) ) ;
2026-04-02 18:14:09 +09:00
assert! ( preflight
. contains ( " Action create a git commit from the current workspace changes " ) ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
}
#[ test ]
fn commit_skipped_report_points_to_next_steps ( ) {
let report = format_commit_skipped_report ( ) ;
assert! ( report . contains ( " Reason no workspace changes " ) ) ;
2026-04-02 18:14:09 +09:00
assert! ( report
. contains ( " Action create a git commit from the current workspace changes " ) ) ;
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
assert! ( report . contains ( " /status to inspect context " ) ) ;
assert! ( report . contains ( " /diff to inspect repo changes " ) ) ;
2026-03-31 20:41:39 +00:00
}
2026-03-31 21:11:57 +00:00
#[ test ]
2026-04-02 18:10:32 +09:00
fn runtime_slash_reports_describe_command_behavior ( ) {
let bughunter = format_bughunter_report ( Some ( " runtime " ) ) ;
assert! ( bughunter . contains ( " Scope runtime " ) ) ;
assert! ( bughunter . contains ( " inspect the selected code for likely bugs " ) ) ;
let ultraplan = format_ultraplan_report ( Some ( " ship the release " ) ) ;
assert! ( ultraplan . contains ( " Task ship the release " ) ) ;
assert! ( ultraplan . contains ( " break work into a multi-step execution plan " ) ) ;
let pr = format_pr_report ( " feature/ux " , Some ( " ready for review " ) ) ;
assert! ( pr . contains ( " Branch feature/ux " ) ) ;
assert! ( pr . contains ( " draft or create a pull request " ) ) ;
let issue = format_issue_report ( Some ( " flaky test " ) ) ;
assert! ( issue . contains ( " Context flaky test " ) ) ;
assert! ( issue . contains ( " draft or create a GitHub issue " ) ) ;
}
#[ test ]
fn no_arg_commands_reject_unexpected_arguments ( ) {
assert! ( validate_no_args ( " /commit " , None ) . is_ok ( ) ) ;
let error = validate_no_args ( " /commit " , Some ( " now " ) )
. expect_err ( " unexpected arguments should fail " )
. to_string ( ) ;
assert! ( error . contains ( " /commit does not accept arguments " ) ) ;
assert! ( error . contains ( " Received: now " ) ) ;
}
2026-03-31 21:11:57 +00:00
#[ test ]
fn config_report_supports_section_views ( ) {
let report = render_config_report ( Some ( " env " ) ) . expect ( " config report should render " ) ;
assert! ( report . contains ( " Merged section: env " ) ) ;
2026-04-01 04:30:28 +00:00
let plugins_report =
render_config_report ( Some ( " plugins " ) ) . expect ( " plugins config report should render " ) ;
assert! ( plugins_report . contains ( " Merged section: plugins " ) ) ;
2026-03-31 21:11:57 +00:00
}
2026-03-31 21:08:19 +00:00
#[ test ]
fn memory_report_uses_sectioned_layout ( ) {
let report = render_memory_report ( ) . expect ( " memory report should render " ) ;
assert! ( report . contains ( " Memory " ) ) ;
assert! ( report . contains ( " Working directory " ) ) ;
assert! ( report . contains ( " Instruction files " ) ) ;
assert! ( report . contains ( " Discovered files " ) ) ;
}
2026-03-31 20:41:39 +00:00
#[ test ]
fn config_report_uses_sectioned_layout ( ) {
2026-03-31 21:11:57 +00:00
let report = render_config_report ( None ) . expect ( " config report should render " ) ;
2026-03-31 20:41:39 +00:00
assert! ( report . contains ( " Config " ) ) ;
assert! ( report . contains ( " Discovered files " ) ) ;
assert! ( report . contains ( " Merged JSON " ) ) ;
2026-03-31 20:22:59 +00:00
}
#[ test ]
2026-03-31 21:06:51 +00:00
fn parses_git_status_metadata ( ) {
2026-04-01 01:10:57 +00:00
let _guard = env_lock ( ) ;
let temp_root = temp_dir ( ) ;
fs ::create_dir_all ( & temp_root ) . expect ( " root dir " ) ;
2026-04-02 11:31:53 +09:00
let ( project_root , branch ) = parse_git_status_metadata_for (
& temp_root ,
Some (
2026-04-01 01:10:57 +00:00
" ## rcc/cli...origin/rcc/cli
2026-03-31 21:06:51 +00:00
M src / main . rs " ,
2026-04-02 11:31:53 +09:00
) ,
) ;
2026-03-31 21:06:51 +00:00
assert_eq! ( branch . as_deref ( ) , Some ( " rcc/cli " ) ) ;
2026-04-01 01:10:57 +00:00
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 ( ) )
) ;
}
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
#[ test ]
fn parses_git_workspace_summary_counts ( ) {
let summary = parse_git_workspace_summary ( Some (
" ## feature/ux
M src / main . rs
M README . md
? ? notes . md
UU conflicted . rs " ,
) ) ;
assert_eq! (
summary ,
GitWorkspaceSummary {
changed_files : 4 ,
staged_files : 2 ,
unstaged_files : 2 ,
untracked_files : 1 ,
conflicted_files : 1 ,
}
) ;
assert_eq! (
summary . headline ( ) ,
" dirty · 4 files · 2 staged, 2 unstaged, 1 untracked, 1 conflicted "
) ;
}
2026-04-01 01:10:57 +00:00
#[ 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 ) ;
2026-04-04 05:33:18 +09:00
let report = render_diff_report_for ( & root ) . expect ( " diff report should render " ) ;
2026-04-01 01:10:57 +00:00
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 " ) ;
2026-04-04 05:33:18 +09:00
let report = render_diff_report_for ( & root ) . expect ( " diff report should render " ) ;
2026-04-01 01:10:57 +00:00
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 " ) ;
2026-04-04 05:33:18 +09:00
let report = render_diff_report_for ( & root ) . expect ( " diff report should render " ) ;
2026-04-01 01:10:57 +00:00
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 " ) ;
2026-03-31 21:06:51 +00:00
}
#[ test ]
2026-03-31 20:22:59 +00:00
fn status_context_reads_real_workspace_metadata ( ) {
let context = status_context ( None ) . expect ( " status context should load " ) ;
assert! ( context . cwd . is_absolute ( ) ) ;
2026-04-01 01:10:57 +00:00
assert! ( context . discovered_config_files > = context . loaded_config_files ) ;
2026-03-31 20:22:59 +00:00
assert! ( context . loaded_config_files < = context . discovered_config_files ) ;
2026-03-31 19:23:05 +00:00
}
2026-03-31 19:27:31 +00:00
#[ test ]
fn normalizes_supported_permission_modes ( ) {
assert_eq! ( normalize_permission_mode ( " read-only " ) , Some ( " read-only " ) ) ;
assert_eq! (
normalize_permission_mode ( " workspace-write " ) ,
Some ( " workspace-write " )
) ;
assert_eq! (
normalize_permission_mode ( " danger-full-access " ) ,
Some ( " danger-full-access " )
) ;
assert_eq! ( normalize_permission_mode ( " unknown " ) , None ) ;
}
2026-03-31 20:42:50 +00:00
#[ test ]
fn clear_command_requires_explicit_confirmation_flag ( ) {
assert_eq! (
SlashCommand ::parse ( " /clear " ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Clear { confirm : false } ) )
2026-03-31 20:42:50 +00:00
) ;
assert_eq! (
SlashCommand ::parse ( " /clear --confirm " ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Clear { confirm : true } ) )
2026-03-31 20:42:50 +00:00
) ;
}
2026-03-31 19:45:25 +00:00
#[ test ]
fn parses_resume_and_config_slash_commands ( ) {
assert_eq! (
2026-04-01 06:15:14 +00:00
SlashCommand ::parse ( " /resume saved-session.jsonl " ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Resume {
2026-04-01 06:15:14 +00:00
session_path : Some ( " saved-session.jsonl " . to_string ( ) )
2026-04-02 18:09:48 +09:00
} ) )
2026-03-31 19:45:25 +00:00
) ;
2026-03-31 20:42:50 +00:00
assert_eq! (
SlashCommand ::parse ( " /clear --confirm " ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Clear { confirm : true } ) )
2026-03-31 20:42:50 +00:00
) ;
2026-03-31 21:11:57 +00:00
assert_eq! (
SlashCommand ::parse ( " /config " ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Config { section : None } ) )
2026-03-31 21:11:57 +00:00
) ;
assert_eq! (
SlashCommand ::parse ( " /config env " ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Config {
2026-03-31 21:11:57 +00:00
section : Some ( " env " . to_string ( ) )
2026-04-02 18:09:48 +09:00
} ) )
2026-03-31 21:11:57 +00:00
) ;
2026-04-02 18:09:48 +09:00
assert_eq! (
SlashCommand ::parse ( " /memory " ) ,
Ok ( Some ( SlashCommand ::Memory ) )
) ;
assert_eq! ( SlashCommand ::parse ( " /init " ) , Ok ( Some ( SlashCommand ::Init ) ) ) ;
2026-04-01 06:15:14 +00:00
assert_eq! (
SlashCommand ::parse ( " /session fork incident-review " ) ,
2026-04-02 18:09:48 +09:00
Ok ( Some ( SlashCommand ::Session {
2026-04-01 06:15:14 +00:00
action : Some ( " fork " . to_string ( ) ) ,
target : Some ( " incident-review " . to_string ( ) )
2026-04-02 18:09:48 +09:00
} ) )
2026-04-01 06:15:14 +00:00
) ;
}
#[ test ]
fn help_mentions_jsonl_resume_examples ( ) {
let mut help = Vec ::new ( ) ;
print_help_to ( & mut help ) . expect ( " help should render " ) ;
let help = String ::from_utf8 ( help ) . expect ( " help should be utf8 " ) ;
2026-04-02 07:15:03 +00:00
assert! ( help . contains ( " claw --resume [SESSION.jsonl|session-id|latest] " ) ) ;
assert! ( help . contains ( " Use `latest` with --resume, /resume, or /session switch " ) ) ;
assert! ( help . contains ( " claw --resume latest " ) ) ;
assert! ( help . contains ( " claw --resume latest /status /diff /export notes.txt " ) ) ;
2026-04-01 06:15:14 +00:00
}
#[ test ]
fn managed_sessions_default_to_jsonl_and_resolve_legacy_json ( ) {
let _guard = cwd_lock ( ) . lock ( ) . expect ( " cwd lock " ) ;
let workspace = temp_workspace ( " session-resolution " ) ;
std ::fs ::create_dir_all ( & workspace ) . expect ( " workspace should create " ) ;
let previous = std ::env ::current_dir ( ) . expect ( " cwd " ) ;
std ::env ::set_current_dir ( & workspace ) . expect ( " switch cwd " ) ;
let handle = create_managed_session_handle ( " session-alpha " ) . expect ( " jsonl handle " ) ;
assert! ( handle . path . ends_with ( " session-alpha.jsonl " ) ) ;
2026-04-02 11:31:53 +09:00
let legacy_path = workspace . join ( " .claw/sessions/legacy.json " ) ;
2026-04-01 06:15:14 +00:00
std ::fs ::create_dir_all (
legacy_path
. parent ( )
. expect ( " legacy path should have parent directory " ) ,
)
. expect ( " session dir should exist " ) ;
Session ::new ( )
. with_persistence_path ( legacy_path . clone ( ) )
. save_to_path ( & legacy_path )
. expect ( " legacy session should save " ) ;
let resolved = resolve_session_reference ( " legacy " ) . expect ( " legacy session should resolve " ) ;
2026-04-02 11:31:53 +09:00
assert_eq! (
Make claw's REPL feel self-explanatory from analysis through commit
Claw already had the core slash-command and git primitives, but the UX
still made users work to discover them, understand current workspace
state, and trust what `/commit` was about to do. This change tightens
that flow in the same places Codex-style CLIs do: command discovery,
live status, typo recovery, and commit preflight/output.
The REPL banner and `/help` now surface a clearer starter path, unknown
slash commands suggest likely matches, `/status` includes actionable git
state, and `/commit` explains what it is staging and committing before
and after the model writes the Lore message. I also cleared the
workspace's existing clippy blockers so the verification lane can stay
fully green.
Constraint: Improve UX inside the existing Rust CLI surfaces without adding new dependencies
Rejected: Add more slash commands first | discoverability and feedback were the bigger friction points
Rejected: Split verification lint fixes into a second commit | user requested one solid commit
Confidence: high
Scope-risk: moderate
Directive: Keep slash discoverability, status reporting, and commit reporting aligned so `/help`, `/status`, and `/commit` tell the same workflow story
Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace
Not-tested: Manual interactive REPL session against live Anthropic/xAI endpoints
2026-04-02 07:20:35 +00:00
resolved
. path
. canonicalize ( )
. expect ( " resolved path should exist " ) ,
legacy_path
. canonicalize ( )
. expect ( " legacy path should exist " )
2026-04-02 11:31:53 +09:00
) ;
2026-04-01 06:15:14 +00:00
std ::env ::set_current_dir ( previous ) . expect ( " restore cwd " ) ;
std ::fs ::remove_dir_all ( workspace ) . expect ( " workspace should clean up " ) ;
}
2026-04-02 07:15:03 +00:00
#[ test ]
fn latest_session_alias_resolves_most_recent_managed_session ( ) {
let _guard = cwd_lock ( ) . lock ( ) . expect ( " cwd lock " ) ;
let workspace = temp_workspace ( " latest-session-alias " ) ;
std ::fs ::create_dir_all ( & workspace ) . expect ( " workspace should create " ) ;
let previous = std ::env ::current_dir ( ) . expect ( " cwd " ) ;
std ::env ::set_current_dir ( & workspace ) . expect ( " switch cwd " ) ;
let older = create_managed_session_handle ( " session-older " ) . expect ( " older handle " ) ;
Session ::new ( )
. with_persistence_path ( older . path . clone ( ) )
. save_to_path ( & older . path )
. expect ( " older session should save " ) ;
std ::thread ::sleep ( Duration ::from_millis ( 20 ) ) ;
let newer = create_managed_session_handle ( " session-newer " ) . expect ( " newer handle " ) ;
Session ::new ( )
. with_persistence_path ( newer . path . clone ( ) )
. save_to_path ( & newer . path )
. expect ( " newer session should save " ) ;
let resolved = resolve_session_reference ( " latest " ) . expect ( " latest session should resolve " ) ;
assert_eq! (
resolved
. path
. canonicalize ( )
. expect ( " resolved path should exist " ) ,
newer . path . canonicalize ( ) . expect ( " newer path should exist " )
) ;
std ::env ::set_current_dir ( previous ) . expect ( " restore cwd " ) ;
std ::fs ::remove_dir_all ( workspace ) . expect ( " workspace should clean up " ) ;
}
#[ test ]
fn unknown_slash_command_guidance_suggests_nearby_commands ( ) {
let message = format_unknown_slash_command ( " stats " ) ;
assert! ( message . contains ( " Unknown slash command: /stats " ) ) ;
assert! ( message . contains ( " /status " ) ) ;
assert! ( message . contains ( " /help " ) ) ;
}
#[ test ]
fn resume_usage_mentions_latest_shortcut ( ) {
let usage = render_resume_usage ( ) ;
assert! ( usage . contains ( " /resume <session-path|session-id|latest> " ) ) ;
assert! ( usage . contains ( " .claw/sessions/<session-id>.jsonl " ) ) ;
assert! ( usage . contains ( " /session list " ) ) ;
}
2026-04-01 06:15:14 +00:00
fn cwd_lock ( ) -> & 'static Mutex < ( ) > {
static LOCK : OnceLock < Mutex < ( ) > > = OnceLock ::new ( ) ;
LOCK . get_or_init ( | | Mutex ::new ( ( ) ) )
}
fn temp_workspace ( label : & str ) -> PathBuf {
let nanos = std ::time ::SystemTime ::now ( )
. duration_since ( std ::time ::UNIX_EPOCH )
. expect ( " system time should be after epoch " )
. as_nanos ( ) ;
std ::env ::temp_dir ( ) . join ( format! ( " claw-cli- {label} - {nanos} " ) )
2026-03-31 19:57:38 +00:00
}
#[ test ]
fn init_template_mentions_detected_rust_workspace ( ) {
2026-04-03 14:31:25 +00:00
let _guard = cwd_lock ( )
. lock ( )
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner ) ;
let workspace_root = PathBuf ::from ( env! ( " CARGO_MANIFEST_DIR " ) ) . join ( " ../.. " ) ;
let rendered = crate ::init ::render_init_claude_md ( & workspace_root ) ;
2026-03-31 19:57:38 +00:00
assert! ( rendered . contains ( " # CLAUDE.md " ) ) ;
assert! ( rendered . contains ( " cargo clippy --workspace --all-targets -- -D warnings " ) ) ;
2026-03-31 19:45:25 +00:00
}
2026-03-31 18:39:39 +00:00
#[ test ]
fn converts_tool_roundtrip_messages ( ) {
let messages = vec! [
ConversationMessage ::user_text ( " hello " ) ,
ConversationMessage ::assistant ( vec! [ ContentBlock ::ToolUse {
id : " tool-1 " . to_string ( ) ,
name : " bash " . to_string ( ) ,
input : " { \" command \" : \" pwd \" } " . to_string ( ) ,
} ] ) ,
ConversationMessage {
role : MessageRole ::Tool ,
blocks : vec ! [ ContentBlock ::ToolResult {
tool_use_id : " tool-1 " . to_string ( ) ,
tool_name : " bash " . to_string ( ) ,
output : " ok " . to_string ( ) ,
is_error : false ,
} ] ,
usage : None ,
} ,
] ;
let converted = super ::convert_messages ( & messages ) ;
assert_eq! ( converted . len ( ) , 3 ) ;
assert_eq! ( converted [ 1 ] . role , " assistant " ) ;
assert_eq! ( converted [ 2 ] . role , " user " ) ;
}
2026-04-01 00:14:38 +00:00
#[ test ]
fn repl_help_mentions_history_completion_and_multiline ( ) {
let help = render_repl_help ( ) ;
assert! ( help . contains ( " Up/Down " ) ) ;
assert! ( help . contains ( " Tab " ) ) ;
assert! ( help . contains ( " Shift+Enter/Ctrl+J " ) ) ;
}
#[ test ]
fn tool_rendering_helpers_compact_output ( ) {
let start = format_tool_call_start ( " read_file " , r # "{"path":"src/main.rs"}"# ) ;
2026-04-01 02:42:49 +00:00
assert! ( start . contains ( " read_file " ) ) ;
2026-04-01 00:14:38 +00:00
assert! ( start . contains ( " src/main.rs " ) ) ;
2026-04-01 03:14:45 +00:00
let done = format_tool_result (
" read_file " ,
r # "{"file":{"filePath":"src/main.rs","content":"hello","numLines":1,"startLine":1,"totalLines":1}}"# ,
false ,
) ;
assert! ( done . contains ( " 📄 Read src/main.rs " ) ) ;
assert! ( done . contains ( " hello " ) ) ;
}
2026-04-01 07:49:20 +00:00
#[ test ]
fn tool_rendering_truncates_large_read_output_for_display_only ( ) {
let content = ( 0 .. 200 )
. map ( | index | format! ( " line {index:03} " ) )
. collect ::< Vec < _ > > ( )
. join ( " \n " ) ;
let output = json! ( {
" file " : {
" filePath " : " src/main.rs " ,
" content " : content ,
" numLines " : 200 ,
" startLine " : 1 ,
" totalLines " : 200
}
} )
. to_string ( ) ;
let rendered = format_tool_result ( " read_file " , & output , false ) ;
assert! ( rendered . contains ( " line 000 " ) ) ;
assert! ( rendered . contains ( " line 079 " ) ) ;
assert! ( ! rendered . contains ( " line 199 " ) ) ;
assert! ( rendered . contains ( " full result preserved in session " ) ) ;
assert! ( output . contains ( " line 199 " ) ) ;
}
#[ test ]
fn tool_rendering_truncates_large_bash_output_for_display_only ( ) {
let stdout = ( 0 .. 120 )
. map ( | index | format! ( " stdout {index:03} " ) )
. collect ::< Vec < _ > > ( )
. join ( " \n " ) ;
let output = json! ( {
" stdout " : stdout ,
" stderr " : " " ,
" returnCodeInterpretation " : " completed successfully "
} )
. to_string ( ) ;
let rendered = format_tool_result ( " bash " , & output , false ) ;
assert! ( rendered . contains ( " stdout 000 " ) ) ;
assert! ( rendered . contains ( " stdout 059 " ) ) ;
assert! ( ! rendered . contains ( " stdout 119 " ) ) ;
assert! ( rendered . contains ( " full result preserved in session " ) ) ;
assert! ( output . contains ( " stdout 119 " ) ) ;
}
2026-04-01 07:53:03 +00:00
#[ test ]
fn tool_rendering_truncates_generic_long_output_for_display_only ( ) {
let items = ( 0 .. 120 )
. map ( | index | format! ( " payload {index:03} " ) )
. collect ::< Vec < _ > > ( ) ;
let output = json! ( {
" summary " : " plugin payload " ,
" items " : items ,
} )
. to_string ( ) ;
let rendered = format_tool_result ( " plugin_echo " , & output , false ) ;
assert! ( rendered . contains ( " plugin_echo " ) ) ;
assert! ( rendered . contains ( " payload 000 " ) ) ;
assert! ( rendered . contains ( " payload 040 " ) ) ;
assert! ( ! rendered . contains ( " payload 080 " ) ) ;
assert! ( ! rendered . contains ( " payload 119 " ) ) ;
assert! ( rendered . contains ( " full result preserved in session " ) ) ;
assert! ( output . contains ( " payload 119 " ) ) ;
}
2026-04-01 08:03:22 +00:00
#[ test ]
fn tool_rendering_truncates_raw_generic_output_for_display_only ( ) {
let output = ( 0 .. 120 )
. map ( | index | format! ( " raw {index:03} " ) )
. collect ::< Vec < _ > > ( )
. join ( " \n " ) ;
let rendered = format_tool_result ( " plugin_echo " , & output , false ) ;
assert! ( rendered . contains ( " plugin_echo " ) ) ;
assert! ( rendered . contains ( " raw 000 " ) ) ;
assert! ( rendered . contains ( " raw 059 " ) ) ;
assert! ( ! rendered . contains ( " raw 119 " ) ) ;
assert! ( rendered . contains ( " full result preserved in session " ) ) ;
assert! ( output . contains ( " raw 119 " ) ) ;
}
2026-04-01 08:10:23 +00:00
#[ test ]
fn ultraplan_progress_lines_include_phase_step_and_elapsed_status ( ) {
let snapshot = InternalPromptProgressState {
command_label : " Ultraplan " ,
task_label : " ship plugin progress " . to_string ( ) ,
step : 3 ,
phase : " running read_file " . to_string ( ) ,
detail : Some ( " reading rust/crates/rusty-claude-cli/src/main.rs " . to_string ( ) ) ,
saw_final_text : false ,
} ;
let started = format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Started ,
& snapshot ,
Duration ::from_secs ( 0 ) ,
None ,
) ;
let heartbeat = format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Heartbeat ,
& snapshot ,
Duration ::from_secs ( 9 ) ,
None ,
) ;
let completed = format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Complete ,
& snapshot ,
Duration ::from_secs ( 12 ) ,
None ,
) ;
let failed = format_internal_prompt_progress_line (
InternalPromptProgressEvent ::Failed ,
& snapshot ,
Duration ::from_secs ( 12 ) ,
Some ( " network timeout " ) ,
) ;
assert! ( started . contains ( " planning started " ) ) ;
assert! ( started . contains ( " current step 3 " ) ) ;
assert! ( heartbeat . contains ( " heartbeat " ) ) ;
assert! ( heartbeat . contains ( " 9s elapsed " ) ) ;
assert! ( heartbeat . contains ( " phase running read_file " ) ) ;
assert! ( completed . contains ( " completed " ) ) ;
assert! ( completed . contains ( " 3 steps total " ) ) ;
assert! ( failed . contains ( " failed " ) ) ;
assert! ( failed . contains ( " network timeout " ) ) ;
}
#[ test ]
fn describe_tool_progress_summarizes_known_tools ( ) {
assert_eq! (
describe_tool_progress ( " read_file " , r # "{"path":"src/main.rs"}"# ) ,
" reading src/main.rs "
) ;
assert! (
describe_tool_progress ( " bash " , r # "{"command":"cargo test -p rusty-claude-cli"}"# )
. contains ( " cargo test -p rusty-claude-cli " )
) ;
assert_eq! (
describe_tool_progress ( " grep_search " , r # "{"pattern":"ultraplan","path":"rust"}"# ) ,
" grep `ultraplan` in rust "
) ;
}
2026-04-01 03:14:45 +00:00
#[ test ]
fn push_output_block_renders_markdown_text ( ) {
let mut out = Vec ::new ( ) ;
let mut events = Vec ::new ( ) ;
2026-04-01 03:55:00 +00:00
let mut pending_tool = None ;
2026-04-01 03:14:45 +00:00
push_output_block (
OutputContentBlock ::Text {
text : " # Heading " . to_string ( ) ,
} ,
& mut out ,
& mut events ,
2026-04-01 03:55:00 +00:00
& mut pending_tool ,
2026-04-01 03:14:45 +00:00
false ,
)
. expect ( " text block should render " ) ;
let rendered = String ::from_utf8 ( out ) . expect ( " utf8 " ) ;
assert! ( rendered . contains ( " Heading " ) ) ;
assert! ( rendered . contains ( '\u{1b}' ) ) ;
2026-04-01 00:14:38 +00:00
}
2026-04-01 02:42:49 +00:00
#[ test ]
fn push_output_block_skips_empty_object_prefix_for_tool_streams ( ) {
let mut out = Vec ::new ( ) ;
let mut events = Vec ::new ( ) ;
2026-04-01 03:55:00 +00:00
let mut pending_tool = None ;
2026-04-01 02:42:49 +00:00
push_output_block (
OutputContentBlock ::ToolUse {
id : " tool-1 " . to_string ( ) ,
name : " read_file " . to_string ( ) ,
input : json ! ( { } ) ,
} ,
& mut out ,
& mut events ,
2026-04-01 03:55:00 +00:00
& mut pending_tool ,
2026-04-01 02:42:49 +00:00
true ,
)
. expect ( " tool block should accumulate " ) ;
assert! ( events . is_empty ( ) ) ;
assert_eq! (
2026-04-01 03:55:00 +00:00
pending_tool ,
2026-04-01 02:42:49 +00:00
Some ( ( " tool-1 " . to_string ( ) , " read_file " . to_string ( ) , String ::new ( ) , ) )
) ;
}
#[ test ]
fn response_to_events_preserves_empty_object_json_input_outside_streaming ( ) {
let mut out = Vec ::new ( ) ;
let events = response_to_events (
MessageResponse {
id : " msg-1 " . to_string ( ) ,
kind : " message " . to_string ( ) ,
model : " claude-opus-4-6 " . to_string ( ) ,
role : " assistant " . to_string ( ) ,
content : vec ! [ OutputContentBlock ::ToolUse {
id : " tool-1 " . to_string ( ) ,
name : " read_file " . to_string ( ) ,
input : json ! ( { } ) ,
} ] ,
stop_reason : Some ( " tool_use " . to_string ( ) ) ,
stop_sequence : None ,
usage : Usage {
input_tokens : 1 ,
output_tokens : 1 ,
cache_creation_input_tokens : 0 ,
cache_read_input_tokens : 0 ,
} ,
request_id : None ,
} ,
& mut out ,
)
. expect ( " response conversion should succeed " ) ;
assert! ( matches! (
& events [ 0 ] ,
AssistantEvent ::ToolUse { name , input , .. }
if name = = " read_file " & & input = = " {} "
) ) ;
}
#[ test ]
fn response_to_events_preserves_non_empty_json_input_outside_streaming ( ) {
let mut out = Vec ::new ( ) ;
let events = response_to_events (
MessageResponse {
id : " msg-2 " . to_string ( ) ,
kind : " message " . to_string ( ) ,
model : " claude-opus-4-6 " . to_string ( ) ,
role : " assistant " . to_string ( ) ,
content : vec ! [ OutputContentBlock ::ToolUse {
id : " tool-2 " . to_string ( ) ,
name : " read_file " . to_string ( ) ,
input : json ! ( { " path " : " rust/Cargo.toml " } ) ,
} ] ,
stop_reason : Some ( " tool_use " . to_string ( ) ) ,
stop_sequence : None ,
usage : Usage {
input_tokens : 1 ,
output_tokens : 1 ,
cache_creation_input_tokens : 0 ,
cache_read_input_tokens : 0 ,
} ,
request_id : None ,
} ,
& mut out ,
)
. expect ( " response conversion should succeed " ) ;
assert! ( matches! (
& events [ 0 ] ,
AssistantEvent ::ToolUse { name , input , .. }
if name = = " read_file " & & input = = " { \" path \" : \" rust/Cargo.toml \" } "
) ) ;
}
2026-04-01 08:06:10 +00:00
#[ test ]
fn response_to_events_ignores_thinking_blocks ( ) {
let mut out = Vec ::new ( ) ;
let events = response_to_events (
MessageResponse {
id : " msg-3 " . to_string ( ) ,
kind : " message " . to_string ( ) ,
model : " claude-opus-4-6 " . to_string ( ) ,
role : " assistant " . to_string ( ) ,
content : vec ! [
OutputContentBlock ::Thinking {
thinking : " step 1 " . to_string ( ) ,
signature : Some ( " sig_123 " . to_string ( ) ) ,
} ,
OutputContentBlock ::Text {
text : " Final answer " . to_string ( ) ,
} ,
] ,
stop_reason : Some ( " end_turn " . to_string ( ) ) ,
stop_sequence : None ,
usage : Usage {
input_tokens : 1 ,
output_tokens : 1 ,
cache_creation_input_tokens : 0 ,
cache_read_input_tokens : 0 ,
} ,
request_id : None ,
} ,
& mut out ,
)
. expect ( " response conversion should succeed " ) ;
assert! ( matches! (
& events [ 0 ] ,
AssistantEvent ::TextDelta ( text ) if text = = " Final answer "
) ) ;
assert! ( ! String ::from_utf8 ( out ) . expect ( " utf8 " ) . contains ( " step 1 " ) ) ;
}
2026-04-02 10:04:54 +00:00
#[ test ]
fn build_runtime_plugin_state_merges_plugin_hooks_into_runtime_features ( ) {
let config_home = temp_dir ( ) ;
let workspace = temp_dir ( ) ;
let source_root = temp_dir ( ) ;
fs ::create_dir_all ( & config_home ) . expect ( " config home " ) ;
fs ::create_dir_all ( & workspace ) . expect ( " workspace " ) ;
fs ::create_dir_all ( & source_root ) . expect ( " source root " ) ;
write_plugin_fixture ( & source_root , " hook-runtime-demo " , true , false ) ;
let mut manager = PluginManager ::new ( PluginManagerConfig ::new ( & config_home ) ) ;
manager
. install ( source_root . to_str ( ) . expect ( " utf8 source path " ) )
. expect ( " plugin install should succeed " ) ;
let loader = ConfigLoader ::new ( & workspace , & config_home ) ;
let runtime_config = loader . load ( ) . expect ( " runtime config should load " ) ;
let state = build_runtime_plugin_state_with_loader ( & workspace , & loader , & runtime_config )
. expect ( " plugin state should load " ) ;
let pre_hooks = state . feature_config . hooks ( ) . pre_tool_use ( ) ;
assert_eq! ( pre_hooks . len ( ) , 1 ) ;
assert! (
pre_hooks [ 0 ] . ends_with ( " hooks/pre.sh " ) ,
" expected installed plugin hook path, got {pre_hooks:?} "
) ;
let _ = fs ::remove_dir_all ( config_home ) ;
let _ = fs ::remove_dir_all ( workspace ) ;
let _ = fs ::remove_dir_all ( source_root ) ;
}
2026-04-03 14:31:25 +00:00
#[ test ]
fn build_runtime_plugin_state_discovers_mcp_tools_and_surfaces_pending_servers ( ) {
let config_home = temp_dir ( ) ;
let workspace = temp_dir ( ) ;
fs ::create_dir_all ( & config_home ) . expect ( " config home " ) ;
fs ::create_dir_all ( & workspace ) . expect ( " workspace " ) ;
let script_path = workspace . join ( " fixture-mcp.py " ) ;
write_mcp_server_fixture ( & script_path ) ;
fs ::write (
config_home . join ( " settings.json " ) ,
format! (
r #" {{
" mcpServers " : { {
" alpha " : { {
" command " : " python3 " ,
" args " : [ " {} " ]
} } ,
" broken " : { {
" command " : " python3 " ,
" args " : [ " -c " , " import sys; sys.exit(0) " ]
} }
} }
} } " #,
script_path . to_string_lossy ( )
) ,
)
. expect ( " write mcp settings " ) ;
let loader = ConfigLoader ::new ( & workspace , & config_home ) ;
let runtime_config = loader . load ( ) . expect ( " runtime config should load " ) ;
let state = build_runtime_plugin_state_with_loader ( & workspace , & loader , & runtime_config )
. expect ( " runtime plugin state should load " ) ;
let allowed = state
. tool_registry
. normalize_allowed_tools ( & [ " mcp__alpha__echo " . to_string ( ) , " MCPTool " . to_string ( ) ] )
. expect ( " mcp tools should be allow-listable " )
. expect ( " allow-list should exist " ) ;
assert! ( allowed . contains ( " mcp__alpha__echo " ) ) ;
assert! ( allowed . contains ( " MCPTool " ) ) ;
let mut executor = CliToolExecutor ::new (
None ,
false ,
state . tool_registry . clone ( ) ,
state . mcp_state . clone ( ) ,
) ;
let tool_output = executor
. execute ( " mcp__alpha__echo " , r # "{"text":"hello"}"# )
. expect ( " discovered mcp tool should execute " ) ;
let tool_json : serde_json ::Value =
serde_json ::from_str ( & tool_output ) . expect ( " tool output should be json " ) ;
assert_eq! ( tool_json [ " structuredContent " ] [ " echoed " ] , " hello " ) ;
let wrapped_output = executor
. execute (
" MCPTool " ,
r # "{"qualifiedName":"mcp__alpha__echo","arguments":{"text":"wrapped"}}"# ,
)
. expect ( " generic mcp wrapper should execute " ) ;
let wrapped_json : serde_json ::Value =
serde_json ::from_str ( & wrapped_output ) . expect ( " wrapped output should be json " ) ;
assert_eq! ( wrapped_json [ " structuredContent " ] [ " echoed " ] , " wrapped " ) ;
let search_output = executor
. execute ( " ToolSearch " , r # "{"query":"alpha echo","max_results":5}"# )
. expect ( " tool search should execute " ) ;
let search_json : serde_json ::Value =
serde_json ::from_str ( & search_output ) . expect ( " search output should be json " ) ;
assert_eq! ( search_json [ " matches " ] [ 0 ] , " mcp__alpha__echo " ) ;
assert_eq! ( search_json [ " pending_mcp_servers " ] [ 0 ] , " broken " ) ;
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
assert_eq! (
search_json [ " mcp_degraded " ] [ " failed_servers " ] [ 0 ] [ " server_name " ] ,
" broken "
) ;
assert_eq! (
search_json [ " mcp_degraded " ] [ " failed_servers " ] [ 0 ] [ " phase " ] ,
" tool_discovery "
) ;
assert_eq! (
search_json [ " mcp_degraded " ] [ " available_tools " ] [ 0 ] ,
" mcp__alpha__echo "
) ;
2026-04-03 14:31:25 +00:00
let listed = executor
. execute ( " ListMcpResourcesTool " , r # "{"server":"alpha"}"# )
. expect ( " resources should list " ) ;
let listed_json : serde_json ::Value =
serde_json ::from_str ( & listed ) . expect ( " resource output should be json " ) ;
assert_eq! ( listed_json [ " resources " ] [ 0 ] [ " uri " ] , " file://guide.txt " ) ;
let read = executor
. execute (
" ReadMcpResourceTool " ,
r # "{"server":"alpha","uri":"file://guide.txt"}"# ,
)
. expect ( " resource should read " ) ;
let read_json : serde_json ::Value =
serde_json ::from_str ( & read ) . expect ( " resource read output should be json " ) ;
assert_eq! (
read_json [ " contents " ] [ 0 ] [ " text " ] ,
" contents for file://guide.txt "
) ;
if let Some ( mcp_state ) = state . mcp_state {
mcp_state
. lock ( )
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
. shutdown ( )
. expect ( " mcp shutdown should succeed " ) ;
}
let _ = fs ::remove_dir_all ( config_home ) ;
let _ = fs ::remove_dir_all ( workspace ) ;
}
feat(mcp+lifecycle): MCP degraded-startup reporting, lane event schema, lane completion hardening
Add MCP structured degraded-startup classification (P2.10):
- classify MCP failures as startup/handshake/config/partial
- expose failed_servers + recovery_recommendations in tool output
- add mcp_degraded output field with server_name, failure_mode, recoverable
Canonical lane event schema (P2.7):
- add LaneEventName variants for all lifecycle states
- wire LaneEvent::new with full 3-arg signature (event, status, emitted_at)
- emit typed events for Started, Blocked, Failed, Finished
Fix let mut executor for search test binary
Fix lane_completion unused import warnings
Note: mcp_stdio::manager_discovery_report test has pre-existing failure on clean main, unrelated to this commit.
2026-04-04 14:31:56 +00:00
#[ test ]
fn build_runtime_plugin_state_surfaces_unsupported_mcp_servers_structurally ( ) {
let config_home = temp_dir ( ) ;
let workspace = temp_dir ( ) ;
fs ::create_dir_all ( & config_home ) . expect ( " config home " ) ;
fs ::create_dir_all ( & workspace ) . expect ( " workspace " ) ;
fs ::write (
config_home . join ( " settings.json " ) ,
r #" {
" mcpServers " : {
" remote " : {
" url " : " https://example.test/mcp "
}
}
} " #,
)
. expect ( " write mcp settings " ) ;
let loader = ConfigLoader ::new ( & workspace , & config_home ) ;
let runtime_config = loader . load ( ) . expect ( " runtime config should load " ) ;
let state = build_runtime_plugin_state_with_loader ( & workspace , & loader , & runtime_config )
. expect ( " runtime plugin state should load " ) ;
let mut executor =
CliToolExecutor ::new ( None , false , state . tool_registry . clone ( ) , state . mcp_state . clone ( ) ) ;
let search_output = executor
. execute ( " ToolSearch " , r # "{"query":"remote","max_results":5}"# )
. expect ( " tool search should execute " ) ;
let search_json : serde_json ::Value =
serde_json ::from_str ( & search_output ) . expect ( " search output should be json " ) ;
assert_eq! ( search_json [ " pending_mcp_servers " ] [ 0 ] , " remote " ) ;
assert_eq! (
search_json [ " mcp_degraded " ] [ " failed_servers " ] [ 0 ] [ " server_name " ] ,
" remote "
) ;
assert_eq! (
search_json [ " mcp_degraded " ] [ " failed_servers " ] [ 0 ] [ " phase " ] ,
" server_registration "
) ;
assert_eq! (
search_json [ " mcp_degraded " ] [ " failed_servers " ] [ 0 ] [ " error " ] [ " context " ] [ " transport " ] ,
" http "
) ;
let _ = fs ::remove_dir_all ( config_home ) ;
let _ = fs ::remove_dir_all ( workspace ) ;
}
2026-04-02 10:04:54 +00:00
#[ test ]
fn build_runtime_runs_plugin_lifecycle_init_and_shutdown ( ) {
let config_home = temp_dir ( ) ;
2026-04-03 05:53:18 +09:00
// Inject a dummy API key so runtime construction succeeds without real credentials.
// This test only exercises plugin lifecycle (init/shutdown), never calls the API.
std ::env ::set_var ( " ANTHROPIC_API_KEY " , " test-dummy-key-for-plugin-lifecycle " ) ;
2026-04-02 10:04:54 +00:00
let workspace = temp_dir ( ) ;
let source_root = temp_dir ( ) ;
fs ::create_dir_all ( & config_home ) . expect ( " config home " ) ;
fs ::create_dir_all ( & workspace ) . expect ( " workspace " ) ;
fs ::create_dir_all ( & source_root ) . expect ( " source root " ) ;
write_plugin_fixture ( & source_root , " lifecycle-runtime-demo " , false , true ) ;
let mut manager = PluginManager ::new ( PluginManagerConfig ::new ( & config_home ) ) ;
let install = manager
. install ( source_root . to_str ( ) . expect ( " utf8 source path " ) )
. expect ( " plugin install should succeed " ) ;
let log_path = install . install_path . join ( " lifecycle.log " ) ;
let loader = ConfigLoader ::new ( & workspace , & config_home ) ;
let runtime_config = loader . load ( ) . expect ( " runtime config should load " ) ;
let runtime_plugin_state =
build_runtime_plugin_state_with_loader ( & workspace , & loader , & runtime_config )
. expect ( " plugin state should load " ) ;
let mut runtime = build_runtime_with_plugin_state (
Session ::new ( ) ,
" runtime-plugin-lifecycle " ,
DEFAULT_MODEL . to_string ( ) ,
vec! [ " test system prompt " . to_string ( ) ] ,
true ,
false ,
None ,
PermissionMode ::DangerFullAccess ,
None ,
runtime_plugin_state ,
)
. expect ( " runtime should build " ) ;
assert_eq! (
fs ::read_to_string ( & log_path ) . expect ( " init log should exist " ) ,
" init \n "
) ;
runtime
. shutdown_plugins ( )
. expect ( " plugin shutdown should succeed " ) ;
assert_eq! (
fs ::read_to_string ( & log_path ) . expect ( " shutdown log should exist " ) ,
" init \n shutdown \n "
) ;
let _ = fs ::remove_dir_all ( config_home ) ;
let _ = fs ::remove_dir_all ( workspace ) ;
let _ = fs ::remove_dir_all ( source_root ) ;
2026-04-03 05:53:18 +09:00
std ::env ::remove_var ( " ANTHROPIC_API_KEY " ) ;
2026-04-02 10:04:54 +00:00
}
feat: merge 2nd round from all rcc/* sessions
- api: tool_use parsing, message_delta, request_id tracking, retry logic
- tools: extended tool suite (WebSearch, WebFetch, Agent, etc.)
- cli: live streamed conversations, session restore, compact commands
- runtime: config loading, system prompt builder, token usage, compaction
2026-03-31 17:43:25 +00:00
}
2026-04-01 01:14:38 +00:00
2026-04-03 14:31:25 +00:00
fn write_mcp_server_fixture ( script_path : & Path ) {
let script = [
" #!/usr/bin/env python3 " ,
" import json, sys " ,
" " ,
" def read_message(): " ,
" header = b'' " ,
r " while not header.endswith(b'\r\n\r\n'):" ,
" chunk = sys.stdin.buffer.read(1) " ,
" if not chunk: " ,
" return None " ,
" header += chunk " ,
" length = 0 " ,
r " for line in header.decode().split('\r\n'):" ,
r " if line.lower().startswith('content-length:'):" ,
" length = int(line.split(':', 1)[1].strip()) " ,
" payload = sys.stdin.buffer.read(length) " ,
" return json.loads(payload.decode()) " ,
" " ,
" def send_message(message): " ,
" payload = json.dumps(message).encode() " ,
r " sys.stdout.buffer.write(f'Content-Length: {len(payload)}\r\n\r\n'.encode() + payload)" ,
" sys.stdout.buffer.flush() " ,
" " ,
" while True: " ,
" request = read_message() " ,
" if request is None: " ,
" break " ,
" method = request['method'] " ,
" if method == 'initialize': " ,
" send_message({ " ,
" 'jsonrpc': '2.0', " ,
" 'id': request['id'], " ,
" 'result': { " ,
" 'protocolVersion': request['params']['protocolVersion'], " ,
" 'capabilities': {'tools': {}, 'resources': {}}, " ,
" 'serverInfo': {'name': 'fixture', 'version': '1.0.0'} " ,
" } " ,
" }) " ,
" elif method == 'tools/list': " ,
" send_message({ " ,
" 'jsonrpc': '2.0', " ,
" 'id': request['id'], " ,
" 'result': { " ,
" 'tools': [ " ,
" { " ,
" 'name': 'echo', " ,
" 'description': 'Echo from MCP fixture', " ,
" 'inputSchema': { " ,
" 'type': 'object', " ,
" 'properties': {'text': {'type': 'string'}}, " ,
" 'required': ['text'], " ,
" 'additionalProperties': False " ,
" }, " ,
" 'annotations': {'readOnlyHint': True} " ,
" } " ,
" ] " ,
" } " ,
" }) " ,
" elif method == 'tools/call': " ,
" args = request['params'].get('arguments') or {} " ,
" send_message({ " ,
" 'jsonrpc': '2.0', " ,
" 'id': request['id'], " ,
" 'result': { " ,
" 'content': [{'type': 'text', 'text': f \" echo:{args.get('text', '')} \" }], " ,
" 'structuredContent': {'echoed': args.get('text', '')}, " ,
" 'isError': False " ,
" } " ,
" }) " ,
" elif method == 'resources/list': " ,
" send_message({ " ,
" 'jsonrpc': '2.0', " ,
" 'id': request['id'], " ,
" 'result': { " ,
" 'resources': [{'uri': 'file://guide.txt', 'name': 'guide', 'mimeType': 'text/plain'}] " ,
" } " ,
" }) " ,
" elif method == 'resources/read': " ,
" uri = request['params']['uri'] " ,
" send_message({ " ,
" 'jsonrpc': '2.0', " ,
" 'id': request['id'], " ,
" 'result': { " ,
" 'contents': [{'uri': uri, 'mimeType': 'text/plain', 'text': f'contents for {uri}'}] " ,
" } " ,
" }) " ,
" else: " ,
" send_message({ " ,
" 'jsonrpc': '2.0', " ,
" 'id': request['id'], " ,
" 'error': {'code': -32601, 'message': method} " ,
" }) " ,
" " ,
]
. join ( " \n " ) ;
fs ::write ( script_path , script ) . expect ( " mcp fixture script should write " ) ;
}
2026-04-01 01:14:38 +00:00
#[ cfg(test) ]
mod sandbox_report_tests {
2026-04-02 11:31:53 +09:00
use super ::{ format_sandbox_report , HookAbortMonitor } ;
use runtime ::HookAbortSignal ;
use std ::sync ::mpsc ;
use std ::time ::Duration ;
2026-04-01 01:14:38 +00:00
#[ test ]
fn sandbox_report_renders_expected_fields ( ) {
let report = format_sandbox_report ( & runtime ::SandboxStatus ::default ( ) ) ;
assert! ( report . contains ( " Sandbox " ) ) ;
assert! ( report . contains ( " Enabled " ) ) ;
assert! ( report . contains ( " Filesystem mode " ) ) ;
assert! ( report . contains ( " Fallback reason " ) ) ;
}
2026-04-02 11:05:03 +09:00
2026-04-01 05:55:24 +00:00
#[ test ]
fn hook_abort_monitor_stops_without_aborting ( ) {
let abort_signal = HookAbortSignal ::new ( ) ;
let ( ready_tx , ready_rx ) = mpsc ::channel ( ) ;
let monitor = HookAbortMonitor ::spawn_with_waiter (
abort_signal . clone ( ) ,
move | stop_rx , abort_signal | {
ready_tx . send ( ( ) ) . expect ( " ready signal " ) ;
let _ = stop_rx . recv ( ) ;
assert! ( ! abort_signal . is_aborted ( ) ) ;
} ,
) ;
ready_rx . recv ( ) . expect ( " waiter should be ready " ) ;
monitor . stop ( ) ;
assert! ( ! abort_signal . is_aborted ( ) ) ;
}
#[ test ]
fn hook_abort_monitor_propagates_interrupt ( ) {
let abort_signal = HookAbortSignal ::new ( ) ;
let ( done_tx , done_rx ) = mpsc ::channel ( ) ;
let monitor = HookAbortMonitor ::spawn_with_waiter (
abort_signal . clone ( ) ,
move | _stop_rx , abort_signal | {
abort_signal . abort ( ) ;
done_tx . send ( ( ) ) . expect ( " done signal " ) ;
} ,
) ;
done_rx
. recv_timeout ( Duration ::from_secs ( 1 ) )
. expect ( " interrupt should complete " ) ;
monitor . stop ( ) ;
assert! ( abort_signal . is_aborted ( ) ) ;
}
2026-04-01 01:14:38 +00:00
}