Skip to content

Commit a6c32ed

Browse files
author
User
committed
fix(commands): wire workspace slash command
1 parent d229a9b commit a6c32ed

2 files changed

Lines changed: 150 additions & 0 deletions

File tree

rust/crates/commands/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,9 @@ pub enum SlashCommand {
11311131
SecurityReview,
11321132
Keybindings,
11331133
PrivacySettings,
1134+
Workspace {
1135+
path: Option<String>,
1136+
},
11341137
Plan {
11351138
mode: Option<String>,
11361139
},
@@ -1272,6 +1275,7 @@ impl SlashCommand {
12721275
Self::SecurityReview => "/security-review",
12731276
Self::Keybindings => "/keybindings",
12741277
Self::PrivacySettings => "/privacy-settings",
1278+
Self::Workspace { .. } => "/workspace",
12751279
Self::Plan { .. } => "/plan",
12761280
Self::Review { .. } => "/review",
12771281
Self::Tasks { .. } => "/tasks",
@@ -1405,6 +1409,9 @@ pub fn validate_slash_command_input(
14051409
validate_no_args(command, &args)?;
14061410
SlashCommand::Setup
14071411
}
1412+
"workspace" | "cwd" => SlashCommand::Workspace {
1413+
path: optional_single_arg(command, &args, "[path]")?,
1414+
},
14081415
"login" | "logout" => {
14091416
return Err(command_error(
14101417
"This auth flow was removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead.",
@@ -5342,6 +5349,7 @@ pub fn handle_slash_command(
53425349
| SlashCommand::Cost
53435350
| SlashCommand::Resume { .. }
53445351
| SlashCommand::Config { .. }
5352+
| SlashCommand::Workspace { .. }
53455353
| SlashCommand::Mcp { .. }
53465354
| SlashCommand::Memory
53475355
| SlashCommand::Init
@@ -6119,6 +6127,24 @@ mod tests {
61196127
assert_eq!(suggest_slash_commands("zzz", 3), Vec::<String>::new());
61206128
}
61216129

6130+
#[test]
6131+
fn parses_workspace_slash_command_and_alias() {
6132+
let workspace = validate_slash_command_input("/workspace")
6133+
.expect("workspace should parse")
6134+
.expect("workspace should be a slash command");
6135+
assert_eq!(workspace, SlashCommand::Workspace { path: None });
6136+
6137+
let cwd = validate_slash_command_input("/cwd src")
6138+
.expect("cwd alias should parse")
6139+
.expect("cwd alias should be a slash command");
6140+
assert_eq!(
6141+
cwd,
6142+
SlashCommand::Workspace {
6143+
path: Some("src".to_string()),
6144+
}
6145+
);
6146+
}
6147+
61226148
#[test]
61236149
fn compacts_sessions_via_slash_command() {
61246150
let mut session = Session::new();

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6654,6 +6654,49 @@ fn run_resume_command(
66546654
message: Some(render_memory_report()?),
66556655
json: Some(render_memory_json()?),
66566656
}),
6657+
SlashCommand::Workspace { path } => {
6658+
let cwd_before = env::current_dir()?;
6659+
let workspace_root = session
6660+
.workspace_root()
6661+
.map(Path::to_path_buf)
6662+
.unwrap_or_else(|| cwd_before.clone());
6663+
let workspace_root = canonicalize_or_clone(&workspace_root);
6664+
let changed = if let Some(path) = path.as_deref() {
6665+
let requested_path = Path::new(path);
6666+
let resolved_path = if requested_path.is_absolute() {
6667+
requested_path.to_path_buf()
6668+
} else {
6669+
cwd_before.join(requested_path)
6670+
};
6671+
let resolved_path = canonicalize_or_clone(&resolved_path);
6672+
if !resolved_path.starts_with(&workspace_root) {
6673+
return Err(format!(
6674+
"workspace_change_outside_root: `{}` is outside the current workspace root `{}`.\nUse `claw --cwd {}` to start a new session there.",
6675+
resolved_path.display(),
6676+
workspace_root.display(),
6677+
resolved_path.display(),
6678+
)
6679+
.into());
6680+
}
6681+
env::set_current_dir(&resolved_path)?;
6682+
true
6683+
} else {
6684+
false
6685+
};
6686+
let cwd_after = env::current_dir()?;
6687+
let message =
6688+
render_workspace_report(&cwd_after, &workspace_root, &session.session_id, changed);
6689+
Ok(ResumeCommandOutcome {
6690+
session: session.clone(),
6691+
message: Some(message),
6692+
json: Some(workspace_report_json(
6693+
&cwd_after,
6694+
&workspace_root,
6695+
&session.session_id,
6696+
changed,
6697+
)),
6698+
})
6699+
}
66576700
SlashCommand::Init => {
66586701
// #142: run the init once, then render both text + structured JSON
66596702
// from the same InitReport so both surfaces stay in sync.
@@ -8122,6 +8165,7 @@ impl LiveCli {
81228165
Self::print_config(section.as_deref())?;
81238166
false
81248167
}
8168+
SlashCommand::Workspace { path } => self.handle_workspace_command(path.as_deref())?,
81258169
SlashCommand::Mcp { action, target } => {
81268170
let args = match (action.as_deref(), target.as_deref()) {
81278171
(None, None) => None,
@@ -8280,6 +8324,51 @@ impl LiveCli {
82808324
);
82818325
}
82828326

8327+
fn handle_workspace_command(
8328+
&mut self,
8329+
target: Option<&str>,
8330+
) -> Result<bool, Box<dyn std::error::Error>> {
8331+
let current_dir = env::current_dir()?;
8332+
let workspace_root = self
8333+
.runtime
8334+
.session()
8335+
.workspace_root()
8336+
.map(Path::to_path_buf)
8337+
.unwrap_or_else(|| current_dir.clone());
8338+
let workspace_root = canonicalize_or_clone(&workspace_root);
8339+
8340+
if let Some(target) = target {
8341+
let requested_path = Path::new(target);
8342+
let resolved_path = if requested_path.is_absolute() {
8343+
requested_path.to_path_buf()
8344+
} else {
8345+
current_dir.join(requested_path)
8346+
};
8347+
let resolved_path = canonicalize_or_clone(&resolved_path);
8348+
if !resolved_path.starts_with(&workspace_root) {
8349+
return Err(format!(
8350+
"workspace_change_outside_root: `{}` is outside the current workspace root `{}`.\nUse `claw --cwd {}` to start a new session there.",
8351+
resolved_path.display(),
8352+
workspace_root.display(),
8353+
resolved_path.display(),
8354+
)
8355+
.into());
8356+
}
8357+
env::set_current_dir(&resolved_path)?;
8358+
}
8359+
8360+
println!(
8361+
"{}",
8362+
render_workspace_report(
8363+
&env::current_dir()?,
8364+
&workspace_root,
8365+
&self.session.id,
8366+
target.is_some(),
8367+
)
8368+
);
8369+
Ok(target.is_some())
8370+
}
8371+
82838372
fn record_prompt_history(&mut self, prompt: &str) {
82848373
let timestamp_ms = std::time::SystemTime::now()
82858374
.duration_since(UNIX_EPOCH)
@@ -9111,6 +9200,41 @@ fn new_cli_session() -> Result<Session, Box<dyn std::error::Error>> {
91119200
Ok(Session::new().with_workspace_root(env::current_dir()?))
91129201
}
91139202

9203+
fn canonicalize_or_clone(path: &Path) -> PathBuf {
9204+
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
9205+
}
9206+
9207+
fn render_workspace_report(
9208+
cwd: &Path,
9209+
workspace_root: &Path,
9210+
session_id: &str,
9211+
changed: bool,
9212+
) -> String {
9213+
let action = if changed { "change" } else { "show" };
9214+
format!(
9215+
"Workspace\n Action {action}\n Session {session_id}\n Workspace root {}\n Current directory {}",
9216+
workspace_root.display(),
9217+
cwd.display(),
9218+
)
9219+
}
9220+
9221+
fn workspace_report_json(
9222+
cwd: &Path,
9223+
workspace_root: &Path,
9224+
session_id: &str,
9225+
changed: bool,
9226+
) -> Value {
9227+
serde_json::json!({
9228+
"kind": "workspace",
9229+
"action": if changed { "change" } else { "show" },
9230+
"status": "ok",
9231+
"session_id": session_id,
9232+
"workspace_root": workspace_root.display().to_string(),
9233+
"current_directory": cwd.display().to_string(),
9234+
"changed": changed,
9235+
})
9236+
}
9237+
91149238
fn create_managed_session_handle(
91159239
session_id: &str,
91169240
) -> Result<SessionHandle, Box<dyn std::error::Error>> {

0 commit comments

Comments
 (0)