From 2b7095e4ae126203c0e30ca84f3a644e56f805c2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 21 Apr 2026 12:55:06 +0900 Subject: [PATCH] feat: surface active session status and session id Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- rust/crates/rusty-claude-cli/src/main.rs | 53 ++++++++++++++-- .../tests/output_format_contract.rs | 63 +++++++++++++++++++ 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 822c609..3475291 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1556,6 +1556,8 @@ fn render_doctor_report() -> Result> { project_root, git_branch, git_summary, + active_session: false, + session_id: None, sandbox_status: resolve_sandbox_status(sandbox_config.sandbox(), &cwd), }; Ok(DoctorReport { @@ -2376,6 +2378,8 @@ struct ResumeCommandOutcome { struct StatusContext { cwd: PathBuf, session_path: Option, + active_session: bool, + session_id: Option, loaded_config_files: usize, discovered_config_files: usize, memory_file_count: usize, @@ -2385,6 +2389,16 @@ struct StatusContext { sandbox_status: runtime::SandboxStatus, } +#[derive(Debug, Clone, Deserialize)] +struct WorkerStateSnapshot { + #[serde(default)] + status: Option, + #[serde(default)] + session_id: Option, + #[serde(default)] + prompt_in_flight: bool, +} + #[derive(Debug, Clone, Copy)] struct StatusUsage { message_count: usize, @@ -4993,6 +5007,8 @@ fn status_json_value( "kind": "status", "model": model, "permission_mode": permission_mode, + "active_session": context.active_session, + "session_id": context.session_id, "usage": { "messages": usage.message_count, "turns": usage.turns, @@ -5051,9 +5067,12 @@ fn status_context( parse_git_status_metadata(project_context.git_status.as_deref()); let git_summary = parse_git_workspace_summary(project_context.git_status.as_deref()); let sandbox_status = resolve_sandbox_status(runtime_config.sandbox(), &cwd); + let worker_state = read_worker_state_snapshot(&cwd); Ok(StatusContext { cwd, session_path: session_path.map(Path::to_path_buf), + active_session: worker_state.as_ref().is_some_and(worker_state_is_active), + session_id: worker_state.and_then(|snapshot| snapshot.session_id), loaded_config_files: runtime_config.loaded_entries().len(), discovered_config_files, memory_file_count: project_context.instruction_files.len(), @@ -5064,6 +5083,20 @@ fn status_context( }) } +fn read_worker_state_snapshot(cwd: &Path) -> Option { + let state_path = cwd.join(".claw").join("worker-state.json"); + let raw = fs::read_to_string(state_path).ok()?; + serde_json::from_str(&raw).ok() +} + +fn worker_state_is_active(snapshot: &WorkerStateSnapshot) -> bool { + snapshot.prompt_in_flight + || matches!( + snapshot.status.as_deref(), + Some("spawning" | "trust_required" | "ready_for_prompt" | "running") + ) +} + fn format_status_report( model: &str, usage: StatusUsage, @@ -5116,10 +5149,7 @@ fn format_status_report( context.git_summary.staged_files, context.git_summary.unstaged_files, context.git_summary.untracked_files, - context.session_path.as_ref().map_or_else( - || "live-repl".to_string(), - |path| path.display().to_string() - ), + format_active_session(context), context.loaded_config_files, context.discovered_config_files, context.memory_file_count, @@ -5133,6 +5163,17 @@ fn format_status_report( ) } +fn format_active_session(context: &StatusContext) -> String { + if context.active_session { + match context.session_id.as_deref() { + Some(session_id) => format!("active ({session_id})"), + None => "active".to_string(), + } + } else { + "idle".to_string() + } +} + fn format_sandbox_report(status: &runtime::SandboxStatus) -> String { format!( "Sandbox @@ -10346,6 +10387,8 @@ mod tests { &super::StatusContext { cwd: PathBuf::from("/tmp/project"), session_path: Some(PathBuf::from("session.jsonl")), + active_session: true, + session_id: Some("boot-status-test".to_string()), loaded_config_files: 2, discovered_config_files: 3, memory_file_count: 4, @@ -10374,10 +10417,10 @@ mod tests { status.contains("Git state dirty · 3 files · 1 staged, 1 unstaged, 1 untracked") ); assert!(status.contains("Changed files 3")); + assert!(status.contains("Session active (boot-status-test)")); assert!(status.contains("Staged 1")); assert!(status.contains("Unstaged 1")); assert!(status.contains("Untracked 1")); - assert!(status.contains("Session session.jsonl")); assert!(status.contains("Config files loaded 2/3")); assert!(status.contains("Memory files 4")); assert!(status.contains("Suggested flow /status → /diff → /commit")); diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 9fbbdcb..ffa6106 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -39,6 +39,8 @@ fn status_and_sandbox_emit_json_when_requested() { let status = assert_json_command(&root, &["--output-format", "json", "status"]); assert_eq!(status["kind"], "status"); + assert_eq!(status["active_session"], false); + assert!(status["session_id"].is_null()); assert!(status["workspace"]["cwd"].as_str().is_some()); let sandbox = assert_json_command(&root, &["--output-format", "json", "sandbox"]); @@ -384,6 +386,47 @@ fn resumed_version_and_init_emit_structured_json_when_requested() { assert!(root.join("CLAUDE.md").exists()); } +#[test] +fn status_json_surfaces_active_session_and_boot_session_id_from_worker_state() { + let root = unique_temp_dir("status-worker-state-json"); + fs::create_dir_all(&root).expect("temp dir should exist"); + write_worker_state_fixture(&root, "running", "boot-fixture-123"); + + let status = assert_json_command(&root, &["--output-format", "json", "status"]); + assert_eq!(status["kind"], "status"); + assert_eq!(status["active_session"], true); + assert_eq!(status["session_id"], "boot-fixture-123"); +} + +#[test] +fn status_text_surfaces_active_session_and_boot_session_id_from_worker_state() { + let root = unique_temp_dir("status-worker-state-text"); + fs::create_dir_all(&root).expect("temp dir should exist"); + write_worker_state_fixture(&root, "running", "boot-fixture-456"); + + let output = run_claw(&root, &["status"], &[]); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("Session active (boot-fixture-456)")); +} + +#[test] +fn worker_state_fixture_round_trips_session_id_across_status_surface() { + let root = unique_temp_dir("status-worker-state-roundtrip"); + fs::create_dir_all(&root).expect("temp dir should exist"); + let session_id = "boot-roundtrip-789"; + write_worker_state_fixture(&root, "running", session_id); + + let status = assert_json_command(&root, &["--output-format", "json", "status"]); + assert_eq!(status["active_session"], true); + assert_eq!(status["session_id"], session_id); + + let raw = fs::read_to_string(root.join(".claw").join("worker-state.json")) + .expect("worker state should exist"); + let state: Value = serde_json::from_str(&raw).expect("worker state should be valid json"); + assert_eq!(state["session_id"], session_id); +} + fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value { assert_json_command_with_env(current_dir, args, &[]) } @@ -431,6 +474,26 @@ fn write_upstream_fixture(root: &Path) -> PathBuf { upstream } +fn write_worker_state_fixture(root: &Path, status: &str, session_id: &str) { + let claw_dir = root.join(".claw"); + fs::create_dir_all(&claw_dir).expect("worker state dir should exist"); + fs::write( + claw_dir.join("worker-state.json"), + serde_json::to_string_pretty(&serde_json::json!({ + "worker_id": "worker-test", + "session_id": session_id, + "status": status, + "is_ready": status == "ready_for_prompt", + "trust_gate_cleared": false, + "prompt_in_flight": status == "running", + "updated_at": 1, + "seconds_since_update": 0 + })) + .expect("worker state json should serialize"), + ) + .expect("worker state fixture should write"); +} + fn write_session_fixture(root: &Path, session_id: &str, user_text: Option<&str>) -> PathBuf { let session_path = root.join("session.jsonl"); let mut session = Session::new()