Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions rust/crates/commands/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,9 @@ pub enum SlashCommand {
SecurityReview,
Keybindings,
PrivacySettings,
Workspace {
path: Option<String>,
},
Plan {
mode: Option<String>,
},
Expand Down Expand Up @@ -1272,6 +1275,7 @@ impl SlashCommand {
Self::SecurityReview => "/security-review",
Self::Keybindings => "/keybindings",
Self::PrivacySettings => "/privacy-settings",
Self::Workspace { .. } => "/workspace",
Self::Plan { .. } => "/plan",
Self::Review { .. } => "/review",
Self::Tasks { .. } => "/tasks",
Expand Down Expand Up @@ -1405,6 +1409,9 @@ pub fn validate_slash_command_input(
validate_no_args(command, &args)?;
SlashCommand::Setup
}
"workspace" | "cwd" => SlashCommand::Workspace {
path: optional_single_arg(command, &args, "[path]")?,
},
"login" | "logout" => {
return Err(command_error(
"This auth flow was removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.",
Expand Down Expand Up @@ -5342,6 +5349,7 @@ pub fn handle_slash_command(
| SlashCommand::Cost
| SlashCommand::Resume { .. }
| SlashCommand::Config { .. }
| SlashCommand::Workspace { .. }
| SlashCommand::Mcp { .. }
| SlashCommand::Memory
| SlashCommand::Init
Expand Down Expand Up @@ -6119,6 +6127,24 @@ mod tests {
assert_eq!(suggest_slash_commands("zzz", 3), Vec::<String>::new());
}

#[test]
fn parses_workspace_slash_command_and_alias() {
let workspace = validate_slash_command_input("/workspace")
.expect("workspace should parse")
.expect("workspace should be a slash command");
assert_eq!(workspace, SlashCommand::Workspace { path: None });

let cwd = validate_slash_command_input("/cwd src")
.expect("cwd alias should parse")
.expect("cwd alias should be a slash command");
assert_eq!(
cwd,
SlashCommand::Workspace {
path: Some("src".to_string()),
}
);
}

#[test]
fn compacts_sessions_via_slash_command() {
let mut session = Session::new();
Expand Down
124 changes: 124 additions & 0 deletions rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6654,6 +6654,49 @@ fn run_resume_command(
message: Some(render_memory_report()?),
json: Some(render_memory_json()?),
}),
SlashCommand::Workspace { path } => {
let cwd_before = env::current_dir()?;
let workspace_root = session
.workspace_root()
.map(Path::to_path_buf)
.unwrap_or_else(|| cwd_before.clone());
let workspace_root = canonicalize_or_clone(&workspace_root);
let changed = if let Some(path) = path.as_deref() {
let requested_path = Path::new(path);
let resolved_path = if requested_path.is_absolute() {
requested_path.to_path_buf()
} else {
cwd_before.join(requested_path)
};
let resolved_path = canonicalize_or_clone(&resolved_path);
if !resolved_path.starts_with(&workspace_root) {
return Err(format!(
"workspace_change_outside_root: `{}` is outside the current workspace root `{}`.\nUse `claw --cwd {}` to start a new session there.",
resolved_path.display(),
workspace_root.display(),
resolved_path.display(),
)
.into());
}
env::set_current_dir(&resolved_path)?;
true
} else {
false
};
let cwd_after = env::current_dir()?;
let message =
render_workspace_report(&cwd_after, &workspace_root, &session.session_id, changed);
Ok(ResumeCommandOutcome {
session: session.clone(),
message: Some(message),
json: Some(workspace_report_json(
&cwd_after,
&workspace_root,
&session.session_id,
changed,
)),
})
}
SlashCommand::Init => {
// #142: run the init once, then render both text + structured JSON
// from the same InitReport so both surfaces stay in sync.
Expand Down Expand Up @@ -8122,6 +8165,7 @@ impl LiveCli {
Self::print_config(section.as_deref())?;
false
}
SlashCommand::Workspace { path } => self.handle_workspace_command(path.as_deref())?,
SlashCommand::Mcp { action, target } => {
let args = match (action.as_deref(), target.as_deref()) {
(None, None) => None,
Expand Down Expand Up @@ -8280,6 +8324,51 @@ impl LiveCli {
);
}

fn handle_workspace_command(
&mut self,
target: Option<&str>,
) -> Result<bool, Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?;
let workspace_root = self
.runtime
.session()
.workspace_root()
.map(Path::to_path_buf)
.unwrap_or_else(|| current_dir.clone());
let workspace_root = canonicalize_or_clone(&workspace_root);

if let Some(target) = target {
let requested_path = Path::new(target);
let resolved_path = if requested_path.is_absolute() {
requested_path.to_path_buf()
} else {
current_dir.join(requested_path)
};
let resolved_path = canonicalize_or_clone(&resolved_path);
if !resolved_path.starts_with(&workspace_root) {
return Err(format!(
"workspace_change_outside_root: `{}` is outside the current workspace root `{}`.\nUse `claw --cwd {}` to start a new session there.",
resolved_path.display(),
workspace_root.display(),
resolved_path.display(),
)
.into());
}
env::set_current_dir(&resolved_path)?;
}

println!(
"{}",
render_workspace_report(
&env::current_dir()?,
&workspace_root,
&self.session.id,
target.is_some(),
)
);
Ok(target.is_some())
}

fn record_prompt_history(&mut self, prompt: &str) {
let timestamp_ms = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
Expand Down Expand Up @@ -9111,6 +9200,41 @@ fn new_cli_session() -> Result<Session, Box<dyn std::error::Error>> {
Ok(Session::new().with_workspace_root(env::current_dir()?))
}

fn canonicalize_or_clone(path: &Path) -> PathBuf {
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}

fn render_workspace_report(
cwd: &Path,
workspace_root: &Path,
session_id: &str,
changed: bool,
) -> String {
let action = if changed { "change" } else { "show" };
format!(
"Workspace\n Action {action}\n Session {session_id}\n Workspace root {}\n Current directory {}",
workspace_root.display(),
cwd.display(),
)
}

fn workspace_report_json(
cwd: &Path,
workspace_root: &Path,
session_id: &str,
changed: bool,
) -> Value {
serde_json::json!({
"kind": "workspace",
"action": if changed { "change" } else { "show" },
"status": "ok",
"session_id": session_id,
"workspace_root": workspace_root.display().to_string(),
"current_directory": cwd.display().to_string(),
"changed": changed,
})
}

fn create_managed_session_handle(
session_id: &str,
) -> Result<SessionHandle, Box<dyn std::error::Error>> {
Expand Down