@@ -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+
91149238fn create_managed_session_handle(
91159239 session_id: &str,
91169240) -> Result<SessionHandle, Box<dyn std::error::Error>> {
0 commit comments