mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-08 21:26:44 +00:00
Compare commits
15 Commits
ece48c7174
...
810036bf09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810036bf09 | ||
|
|
0f34c66acd | ||
|
|
6af0189906 | ||
|
|
b95d330310 | ||
|
|
74311cc511 | ||
|
|
6ae8850d45 | ||
|
|
ef9439d772 | ||
|
|
4f670e5513 | ||
|
|
8dcf10361f | ||
|
|
cf129c8793 | ||
|
|
c0248253ac | ||
|
|
1e14d59a71 | ||
|
|
11e2353585 | ||
|
|
0845705639 | ||
|
|
316864227c |
12
ROADMAP.md
12
ROADMAP.md
@@ -532,3 +532,15 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
||||
52. **`cargo install claw-code` false-positive install: deprecated stub silently succeeds** — dogfooded 2026-04-10 via #claw-code. User runs `cargo install claw-code`, install succeeds, Cargo places `claw-code-deprecated.exe`, user runs `claw` and gets `command not found`. The deprecated binary only prints `"claw-code has been renamed to agent-code"`. The success signal is false-positive: install appears to work but leaves the user with no working `claw` binary. Fix shape: (a) README must warn explicitly against `cargo install claw-code` with the hyphen (current note only warns about `clawcode` without hyphen); (b) if the deprecated crate is in our control, update its binary to print a clearer redirect message including `cargo install agent-code`; (c) ensure the Windows setup doc path mentions `agent-code` explicitly. Source: user in #claw-code 2026-04-10; traced by gaebal-gajae.
|
||||
|
||||
53. **`cargo install agent-code` produces `agent.exe`, not `agent-code.exe` — binary name mismatch in docs** — dogfooded 2026-04-10 via #claw-code. User follows the `claw-code` rename hint to run `cargo install agent-code`, install succeeds, but the installed binary is `agent.exe` (Unix: `agent`), not `agent-code` or `agent-code.exe`. User tries `agent-code --version`, gets `command not found`, concludes install is broken. The package name (`agent-code`), the crate name, and the installed binary name (`agent`) are all different. Fix shape: docs must show the full chain explicitly: `cargo install agent-code` → run via `agent` (Unix) / `agent.exe` (Windows). ROADMAP #52 note updated with corrected binary name. Source: user in #claw-code 2026-04-10; traced by gaebal-gajae.
|
||||
|
||||
54. **Circular "Did you mean /X?" error for spec-registered commands with no parse arm** — dogfooded 2026-04-10. 23 commands in the spec (shown in `/help` output) had no parse arm in `validate_slash_command_input`, so typing them produced `"Unknown slash command: /X — Did you mean /X?"`. The "Did you mean" suggestion pointed at the exact command the user just typed. Root cause: spec registration and parse-arm implementation were independent — a command could appear in help and completions without being parseable. **Done at `1e14d59` 2026-04-10**: added all 23 to STUB_COMMANDS and added pre-parse intercept in resume dispatch. Source: Jobdori dogfood.
|
||||
|
||||
55. **`/session list` unsupported in resume mode despite only needing directory read** — dogfooded 2026-04-10. `/session list` in `--output-format json --resume` mode returned `"unsupported resumed slash command"`. The command only reads the sessions directory — no live runtime needed. **Done at `8dcf103` 2026-04-10**: added `Session{action:"list"}` arm in `run_resume_command()`. Emits `{kind:session_list, sessions:[...ids], active:<id>}`. Partial progress on ROADMAP #21. Source: Jobdori dogfood.
|
||||
|
||||
56. **`--resume` with no command ignores `--output-format json`** — dogfooded 2026-04-10. `claw --output-format json --resume <session>` (no slash command) printed prose `"Restored session from <path> (N messages)."` to stdout, ignoring the JSON output format flag. **Done at `4f670e5` 2026-04-10**: empty-commands path now emits `{kind:restored, session_id, path, message_count}` in JSON mode. Source: Jobdori dogfood.
|
||||
|
||||
57. **Session load errors bypass `--output-format json` — prose error on corrupt JSONL** — dogfooded 2026-04-10. `claw --output-format json --resume <corrupt.jsonl> /status` printed bare prose `"failed to restore session: ..."` to stderr, not a JSON error object. Both the path-resolution and JSONL-load error paths ignored `output_format`. **Done at `cf129c8` 2026-04-10**: both paths now emit `{type:error, error:"failed to restore session: <detail>"}` in JSON mode. Source: Jobdori dogfood.
|
||||
|
||||
58. **Windows startup crash: `HOME is not set`** — user report 2026-04-10 in #claw-code (MaxDerVerpeilte). On Windows, `HOME` is often unset — `USERPROFILE` is the native equivalent. Four code paths only checked `HOME`: `config_home_dir()` (tools), `credentials_home_dir()` (runtime/oauth), `detect_broad_cwd()` (CLI), and skill lookup roots (tools). All crashed or silently skipped on stock Windows installs. **Done at `b95d330` 2026-04-10**: all four paths now fall back to `USERPROFILE` when `HOME` is absent. Error message updated to suggest `USERPROFILE` or `CLAW_CONFIG_HOME`. Source: MaxDerVerpeilte in #claw-code.
|
||||
|
||||
59. **Session metadata does not persist the model used** — dogfooded 2026-04-10. When resuming a session, `/status` reports `model: null` because the session JSONL stores no model field. A claw resuming a session cannot tell what model was originally used. The model is only known at runtime construction time via CLI flag or config. Fix shape: persist `model` in the session metadata record on first API call, and surface it in resumed `/status`. Source: Jobdori dogfood.
|
||||
|
||||
@@ -1652,6 +1652,7 @@ mod tests {
|
||||
}"#;
|
||||
|
||||
use super::deserialize_null_as_empty_vec;
|
||||
#[allow(dead_code)]
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct Delta {
|
||||
content: Option<String>,
|
||||
@@ -1666,7 +1667,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Regression: when building a multi-turn request where a prior assistant
|
||||
/// turn has no tool calls, the serialized assistant message must NOT include
|
||||
/// `tool_calls: []`. Some providers reject requests that carry an empty
|
||||
|
||||
@@ -555,7 +555,7 @@ fn extract_summary_timeline(summary: &str) -> Vec<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
collect_key_files, compact_session, estimate_session_tokens, format_compact_summary,
|
||||
collect_key_files, compact_session, format_compact_summary,
|
||||
get_compact_continuation_message, infer_pending_work, should_compact, CompactionConfig,
|
||||
};
|
||||
use crate::session::{ContentBlock, ConversationMessage, MessageRole, Session};
|
||||
|
||||
@@ -335,7 +335,14 @@ fn credentials_home_dir() -> io::Result<PathBuf> {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
let home = std::env::var_os("HOME")
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "HOME is not set"))?;
|
||||
.or_else(|| std::env::var_os("USERPROFILE"))
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"HOME is not set (on Windows, set USERPROFILE or HOME, \
|
||||
or use CLAW_CONFIG_HOME to point directly at the config directory)",
|
||||
)
|
||||
})?;
|
||||
Ok(PathBuf::from(home).join(".claw"))
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ pub struct Session {
|
||||
pub fork: Option<SessionFork>,
|
||||
pub workspace_root: Option<PathBuf>,
|
||||
pub prompt_history: Vec<SessionPromptEntry>,
|
||||
/// The model used in this session, persisted so resumed sessions can
|
||||
/// report which model was originally used.
|
||||
pub model: Option<String>,
|
||||
persistence: Option<SessionPersistence>,
|
||||
}
|
||||
|
||||
@@ -161,6 +164,7 @@ impl Session {
|
||||
fork: None,
|
||||
workspace_root: None,
|
||||
prompt_history: Vec::new(),
|
||||
model: None,
|
||||
persistence: None,
|
||||
}
|
||||
}
|
||||
@@ -263,6 +267,7 @@ impl Session {
|
||||
}),
|
||||
workspace_root: self.workspace_root.clone(),
|
||||
prompt_history: self.prompt_history.clone(),
|
||||
model: self.model.clone(),
|
||||
persistence: None,
|
||||
}
|
||||
}
|
||||
@@ -371,6 +376,10 @@ impl Session {
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let model = object
|
||||
.get("model")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(String::from);
|
||||
Ok(Self {
|
||||
version,
|
||||
session_id,
|
||||
@@ -381,6 +390,7 @@ impl Session {
|
||||
fork,
|
||||
workspace_root,
|
||||
prompt_history,
|
||||
model,
|
||||
persistence: None,
|
||||
})
|
||||
}
|
||||
@@ -394,6 +404,7 @@ impl Session {
|
||||
let mut compaction = None;
|
||||
let mut fork = None;
|
||||
let mut workspace_root = None;
|
||||
let mut model = None;
|
||||
let mut prompt_history = Vec::new();
|
||||
|
||||
for (line_number, raw_line) in contents.lines().enumerate() {
|
||||
@@ -433,6 +444,10 @@ impl Session {
|
||||
.get("workspace_root")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(PathBuf::from);
|
||||
model = object
|
||||
.get("model")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(String::from);
|
||||
}
|
||||
"message" => {
|
||||
let message_value = object.get("message").ok_or_else(|| {
|
||||
@@ -475,6 +490,7 @@ impl Session {
|
||||
fork,
|
||||
workspace_root,
|
||||
prompt_history,
|
||||
model,
|
||||
persistence: None,
|
||||
})
|
||||
}
|
||||
@@ -580,6 +596,9 @@ impl Session {
|
||||
JsonValue::String(workspace_root_to_string(workspace_root)?),
|
||||
);
|
||||
}
|
||||
if let Some(model) = &self.model {
|
||||
object.insert("model".to_string(), JsonValue::String(model.clone()));
|
||||
}
|
||||
Ok(JsonValue::Object(object))
|
||||
}
|
||||
|
||||
|
||||
@@ -2221,7 +2221,17 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
match resolve_session_reference(&session_path.display().to_string()) {
|
||||
Ok(handle) => handle.path,
|
||||
Err(error) => {
|
||||
eprintln!("failed to restore session: {error}");
|
||||
if output_format == CliOutputFormat::Json {
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"error": format!("failed to restore session: {error}"),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
eprintln!("failed to restore session: {error}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -2230,22 +2240,71 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
||||
let session = match Session::load_from_path(&resolved_path) {
|
||||
Ok(session) => session,
|
||||
Err(error) => {
|
||||
eprintln!("failed to restore session: {error}");
|
||||
if output_format == CliOutputFormat::Json {
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"error": format!("failed to restore session: {error}"),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
eprintln!("failed to restore session: {error}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if commands.is_empty() {
|
||||
println!(
|
||||
"Restored session from {} ({} messages).",
|
||||
resolved_path.display(),
|
||||
session.messages.len()
|
||||
);
|
||||
if output_format == CliOutputFormat::Json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"kind": "restored",
|
||||
"session_id": session.session_id,
|
||||
"path": resolved_path.display().to_string(),
|
||||
"message_count": session.messages.len(),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"Restored session from {} ({} messages).",
|
||||
resolved_path.display(),
|
||||
session.messages.len()
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut session = session;
|
||||
for raw_command in commands {
|
||||
// Intercept spec commands that have no parse arm before calling
|
||||
// SlashCommand::parse — they return Err(SlashCommandParseError) which
|
||||
// formats as the confusing circular "Did you mean /X?" message.
|
||||
// STUB_COMMANDS covers both completions-filtered stubs and parse-less
|
||||
// spec entries; treat both as unsupported in resume mode.
|
||||
{
|
||||
let cmd_root = raw_command
|
||||
.trim_start_matches('/')
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("");
|
||||
if STUB_COMMANDS.contains(&cmd_root) {
|
||||
if output_format == CliOutputFormat::Json {
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"error": format!("/{cmd_root} is not yet implemented in this build"),
|
||||
"command": raw_command,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
eprintln!("/{cmd_root} is not yet implemented in this build");
|
||||
}
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
let command = match SlashCommand::parse(raw_command) {
|
||||
Ok(Some(command)) => command,
|
||||
Ok(None) => {
|
||||
@@ -2662,7 +2721,7 @@ fn run_resume_command(
|
||||
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(render_repl_help()),
|
||||
json: None,
|
||||
json: Some(serde_json::json!({ "kind": "help", "text": render_repl_help() })),
|
||||
}),
|
||||
SlashCommand::Compact => {
|
||||
let result = runtime::compact_session(
|
||||
@@ -2730,7 +2789,7 @@ fn run_resume_command(
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(format_status_report(
|
||||
"restored-session",
|
||||
session.model.as_deref().unwrap_or("restored-session"),
|
||||
StatusUsage {
|
||||
message_count: session.messages.len(),
|
||||
turns: tracker.turns(),
|
||||
@@ -2742,7 +2801,7 @@ fn run_resume_command(
|
||||
&context,
|
||||
)),
|
||||
json: Some(status_json_value(
|
||||
None,
|
||||
session.model.as_deref(),
|
||||
StatusUsage {
|
||||
message_count: session.messages.len(),
|
||||
turns: tracker.turns(),
|
||||
@@ -2817,13 +2876,16 @@ fn run_resume_command(
|
||||
json: Some(init_json_value(&message)),
|
||||
})
|
||||
}
|
||||
SlashCommand::Diff => Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(render_diff_report_for(
|
||||
&std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
)?),
|
||||
json: None,
|
||||
}),
|
||||
SlashCommand::Diff => {
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let message = render_diff_report_for(&cwd)?;
|
||||
let json = render_diff_json_for(&cwd)?;
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(message),
|
||||
json: Some(json),
|
||||
})
|
||||
}
|
||||
SlashCommand::Version => Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(render_version_report()),
|
||||
@@ -2832,14 +2894,19 @@ fn run_resume_command(
|
||||
SlashCommand::Export { path } => {
|
||||
let export_path = resolve_export_path(path.as_deref(), session)?;
|
||||
fs::write(&export_path, render_export_text(session))?;
|
||||
let msg_count = session.messages.len();
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(format!(
|
||||
"Export\n Result wrote transcript\n File {}\n Messages {}",
|
||||
export_path.display(),
|
||||
session.messages.len(),
|
||||
msg_count,
|
||||
)),
|
||||
json: None,
|
||||
json: Some(serde_json::json!({
|
||||
"kind": "export",
|
||||
"file": export_path.display().to_string(),
|
||||
"message_count": msg_count,
|
||||
})),
|
||||
})
|
||||
}
|
||||
SlashCommand::Agents { args } => {
|
||||
@@ -2847,7 +2914,10 @@ fn run_resume_command(
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(handle_agents_slash_command(args.as_deref(), &cwd)?),
|
||||
json: None,
|
||||
json: Some(serde_json::json!({
|
||||
"kind": "agents",
|
||||
"text": handle_agents_slash_command(args.as_deref(), &cwd)?,
|
||||
})),
|
||||
})
|
||||
}
|
||||
SlashCommand::Skills { args } => {
|
||||
@@ -2906,6 +2976,25 @@ fn run_resume_command(
|
||||
})
|
||||
}
|
||||
SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()),
|
||||
// /session list can be served from the sessions directory without a live session.
|
||||
SlashCommand::Session {
|
||||
action: Some(ref act),
|
||||
..
|
||||
} if act == "list" => {
|
||||
let sessions = list_managed_sessions().unwrap_or_default();
|
||||
let session_ids: Vec<String> = sessions.iter().map(|s| s.id.clone()).collect();
|
||||
let active_id = session.session_id.clone();
|
||||
let text = render_session_list(&active_id).unwrap_or_else(|e| format!("error: {e}"));
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(text),
|
||||
json: Some(serde_json::json!({
|
||||
"kind": "session_list",
|
||||
"sessions": session_ids,
|
||||
"active": active_id,
|
||||
})),
|
||||
})
|
||||
}
|
||||
SlashCommand::Bughunter { .. }
|
||||
| SlashCommand::Commit { .. }
|
||||
| SlashCommand::Pr { .. }
|
||||
@@ -2966,6 +3055,7 @@ fn detect_broad_cwd() -> Option<PathBuf> {
|
||||
return None;
|
||||
};
|
||||
let is_home = env::var_os("HOME")
|
||||
.or_else(|| env::var_os("USERPROFILE"))
|
||||
.map(|h| PathBuf::from(h) == cwd)
|
||||
.unwrap_or(false);
|
||||
let is_root = cwd.parent().is_none();
|
||||
@@ -5554,6 +5644,30 @@ fn render_diff_report_for(cwd: &Path) -> Result<String, Box<dyn std::error::Erro
|
||||
Ok(format!("Diff\n\n{}", sections.join("\n\n")))
|
||||
}
|
||||
|
||||
fn render_diff_json_for(cwd: &Path) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
||||
let in_git_repo = std::process::Command::new("git")
|
||||
.args(["rev-parse", "--is-inside-work-tree"])
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
if !in_git_repo {
|
||||
return Ok(serde_json::json!({
|
||||
"kind": "diff",
|
||||
"result": "no_git_repo",
|
||||
"detail": format!("{} is not inside a git project", cwd.display()),
|
||||
}));
|
||||
}
|
||||
let staged = run_git_diff_command_in(cwd, &["diff", "--cached"])?;
|
||||
let unstaged = run_git_diff_command_in(cwd, &["diff"])?;
|
||||
Ok(serde_json::json!({
|
||||
"kind": "diff",
|
||||
"result": if staged.trim().is_empty() && unstaged.trim().is_empty() { "clean" } else { "changes" },
|
||||
"staged": staged.trim(),
|
||||
"unstaged": unstaged.trim(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn run_git_diff_command_in(
|
||||
cwd: &Path,
|
||||
args: &[&str],
|
||||
@@ -6638,7 +6752,7 @@ fn build_runtime(
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_runtime_with_plugin_state(
|
||||
session: Session,
|
||||
mut session: Session,
|
||||
session_id: &str,
|
||||
model: String,
|
||||
system_prompt: Vec<String>,
|
||||
@@ -6649,6 +6763,10 @@ fn build_runtime_with_plugin_state(
|
||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||
runtime_plugin_state: RuntimePluginState,
|
||||
) -> Result<BuiltRuntime, Box<dyn std::error::Error>> {
|
||||
// Persist the model in session metadata so resumed sessions can report it.
|
||||
if session.model.is_none() {
|
||||
session.model = Some(model.clone());
|
||||
}
|
||||
let RuntimePluginState {
|
||||
feature_config,
|
||||
tool_registry,
|
||||
@@ -7285,7 +7403,6 @@ const STUB_COMMANDS: &[&str] = &[
|
||||
"logout",
|
||||
"vim",
|
||||
"upgrade",
|
||||
"stats",
|
||||
"share",
|
||||
"feedback",
|
||||
"files",
|
||||
@@ -7320,6 +7437,78 @@ const STUB_COMMANDS: &[&str] = &[
|
||||
"tag",
|
||||
"output-style",
|
||||
"add-dir",
|
||||
// Spec entries with no parse arm — produce circular "Did you mean" error
|
||||
// without this guard. Adding here routes them to the proper unsupported
|
||||
// message and excludes them from REPL completions / help.
|
||||
// NOTE: do NOT add "stats", "tokens", "cache" — they are implemented.
|
||||
"allowed-tools",
|
||||
"bookmarks",
|
||||
"workspace",
|
||||
"reasoning",
|
||||
"budget",
|
||||
"rate-limit",
|
||||
"changelog",
|
||||
"diagnostics",
|
||||
"metrics",
|
||||
"tool-details",
|
||||
"focus",
|
||||
"unfocus",
|
||||
"pin",
|
||||
"unpin",
|
||||
"language",
|
||||
"profile",
|
||||
"max-tokens",
|
||||
"temperature",
|
||||
"system-prompt",
|
||||
"notifications",
|
||||
"telemetry",
|
||||
"env",
|
||||
"project",
|
||||
"terminal-setup",
|
||||
"api-key",
|
||||
"reset",
|
||||
"undo",
|
||||
"stop",
|
||||
"retry",
|
||||
"paste",
|
||||
"screenshot",
|
||||
"image",
|
||||
"search",
|
||||
"listen",
|
||||
"speak",
|
||||
"format",
|
||||
"test",
|
||||
"lint",
|
||||
"build",
|
||||
"run",
|
||||
"git",
|
||||
"stash",
|
||||
"blame",
|
||||
"log",
|
||||
"cron",
|
||||
"team",
|
||||
"benchmark",
|
||||
"migrate",
|
||||
"templates",
|
||||
"explain",
|
||||
"refactor",
|
||||
"docs",
|
||||
"fix",
|
||||
"perf",
|
||||
"chat",
|
||||
"web",
|
||||
"map",
|
||||
"symbols",
|
||||
"references",
|
||||
"definition",
|
||||
"hover",
|
||||
"autofix",
|
||||
"multi",
|
||||
"macro",
|
||||
"alias",
|
||||
"parallel",
|
||||
"subagent",
|
||||
"agent",
|
||||
];
|
||||
|
||||
fn slash_command_completion_candidates_with_sessions(
|
||||
|
||||
@@ -253,7 +253,8 @@ fn doctor_and_resume_status_emit_json_when_requested() {
|
||||
],
|
||||
);
|
||||
assert_eq!(resumed["kind"], "status");
|
||||
assert_eq!(resumed["model"], "restored-session");
|
||||
// model is null in resume mode (not known without --model flag)
|
||||
assert!(resumed["model"].is_null());
|
||||
assert_eq!(resumed["usage"]["messages"], 1);
|
||||
assert!(resumed["workspace"]["cwd"].as_str().is_some());
|
||||
assert!(resumed["sandbox"]["filesystem_mode"].as_str().is_some());
|
||||
|
||||
@@ -261,7 +261,8 @@ fn resumed_status_command_emits_structured_json_when_requested() {
|
||||
let parsed: Value =
|
||||
serde_json::from_str(stdout.trim()).expect("resume status output should be json");
|
||||
assert_eq!(parsed["kind"], "status");
|
||||
assert_eq!(parsed["model"], "restored-session");
|
||||
// model is null in resume mode (not known without --model flag)
|
||||
assert!(parsed["model"].is_null());
|
||||
assert_eq!(parsed["permission_mode"], "danger-full-access");
|
||||
assert_eq!(parsed["usage"]["messages"], 1);
|
||||
assert!(parsed["usage"]["turns"].is_number());
|
||||
@@ -275,6 +276,47 @@ fn resumed_status_command_emits_structured_json_when_requested() {
|
||||
assert!(parsed["sandbox"]["filesystem_mode"].as_str().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_status_surfaces_persisted_model() {
|
||||
// given — create a session with model already set
|
||||
let temp_dir = unique_temp_dir("resume-status-model");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
|
||||
let mut session = Session::new();
|
||||
session.model = Some("claude-sonnet-4-6".to_string());
|
||||
session
|
||||
.push_user_text("model persistence fixture")
|
||||
.expect("write ok");
|
||||
session.save_to_path(&session_path).expect("persist ok");
|
||||
|
||||
// when
|
||||
let output = run_claw(
|
||||
&temp_dir,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"--resume",
|
||||
session_path.to_str().expect("utf8 path"),
|
||||
"/status",
|
||||
],
|
||||
);
|
||||
|
||||
// then
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
let parsed: Value = serde_json::from_str(stdout.trim()).expect("should be json");
|
||||
assert_eq!(parsed["kind"], "status");
|
||||
assert_eq!(
|
||||
parsed["model"], "claude-sonnet-4-6",
|
||||
"model should round-trip through session metadata"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_sandbox_command_emits_structured_json_when_requested() {
|
||||
// given
|
||||
@@ -318,6 +360,175 @@ fn resumed_sandbox_command_emits_structured_json_when_requested() {
|
||||
assert!(parsed["markers"].is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_version_command_emits_structured_json() {
|
||||
let temp_dir = unique_temp_dir("resume-version-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
Session::new()
|
||||
.save_to_path(&session_path)
|
||||
.expect("session should persist");
|
||||
|
||||
let output = run_claw(
|
||||
&temp_dir,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"--resume",
|
||||
session_path.to_str().expect("utf8 path"),
|
||||
"/version",
|
||||
],
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
let parsed: Value = serde_json::from_str(stdout.trim()).expect("should be json");
|
||||
assert_eq!(parsed["kind"], "version");
|
||||
assert!(parsed["version"].as_str().is_some());
|
||||
assert!(parsed["git_sha"].as_str().is_some());
|
||||
assert!(parsed["target"].as_str().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_export_command_emits_structured_json() {
|
||||
let temp_dir = unique_temp_dir("resume-export-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
let mut session = Session::new();
|
||||
session
|
||||
.push_user_text("export json fixture")
|
||||
.expect("write ok");
|
||||
session.save_to_path(&session_path).expect("persist ok");
|
||||
|
||||
let output = run_claw(
|
||||
&temp_dir,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"--resume",
|
||||
session_path.to_str().expect("utf8 path"),
|
||||
"/export",
|
||||
],
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
let parsed: Value = serde_json::from_str(stdout.trim()).expect("should be json");
|
||||
assert_eq!(parsed["kind"], "export");
|
||||
assert!(parsed["file"].as_str().is_some());
|
||||
assert_eq!(parsed["message_count"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_help_command_emits_structured_json() {
|
||||
let temp_dir = unique_temp_dir("resume-help-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
Session::new()
|
||||
.save_to_path(&session_path)
|
||||
.expect("persist ok");
|
||||
|
||||
let output = run_claw(
|
||||
&temp_dir,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"--resume",
|
||||
session_path.to_str().expect("utf8 path"),
|
||||
"/help",
|
||||
],
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
let parsed: Value = serde_json::from_str(stdout.trim()).expect("should be json");
|
||||
assert_eq!(parsed["kind"], "help");
|
||||
assert!(parsed["text"].as_str().is_some());
|
||||
let text = parsed["text"].as_str().unwrap();
|
||||
assert!(text.contains("/status"), "help text should list /status");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_no_command_emits_restored_json() {
|
||||
let temp_dir = unique_temp_dir("resume-no-cmd-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
let mut session = Session::new();
|
||||
session
|
||||
.push_user_text("restored json fixture")
|
||||
.expect("write ok");
|
||||
session.save_to_path(&session_path).expect("persist ok");
|
||||
|
||||
let output = run_claw(
|
||||
&temp_dir,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"--resume",
|
||||
session_path.to_str().expect("utf8 path"),
|
||||
],
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("utf8");
|
||||
let parsed: Value = serde_json::from_str(stdout.trim()).expect("should be json");
|
||||
assert_eq!(parsed["kind"], "restored");
|
||||
assert!(parsed["session_id"].as_str().is_some());
|
||||
assert!(parsed["path"].as_str().is_some());
|
||||
assert_eq!(parsed["message_count"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_stub_command_emits_not_implemented_json() {
|
||||
let temp_dir = unique_temp_dir("resume-stub-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
Session::new()
|
||||
.save_to_path(&session_path)
|
||||
.expect("persist ok");
|
||||
|
||||
let output = run_claw(
|
||||
&temp_dir,
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"--resume",
|
||||
session_path.to_str().expect("utf8 path"),
|
||||
"/allowed-tools",
|
||||
],
|
||||
);
|
||||
|
||||
// Stub commands exit with code 2
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8(output.stderr).expect("utf8");
|
||||
let parsed: Value = serde_json::from_str(stderr.trim()).expect("should be json");
|
||||
assert_eq!(parsed["type"], "error");
|
||||
assert!(
|
||||
parsed["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("not yet implemented"),
|
||||
"error should say not yet implemented: {:?}",
|
||||
parsed["error"]
|
||||
);
|
||||
}
|
||||
|
||||
fn run_claw(current_dir: &Path, args: &[&str]) -> Output {
|
||||
run_claw_with_env(current_dir, args, &[])
|
||||
}
|
||||
|
||||
@@ -3066,7 +3066,7 @@ fn skill_lookup_roots() -> Vec<SkillLookupRoot> {
|
||||
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
|
||||
push_prefixed_skill_lookup_roots(&mut roots, std::path::Path::new(&codex_home));
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
if let Ok(home) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
|
||||
push_home_skill_lookup_roots(&mut roots, std::path::Path::new(&home));
|
||||
}
|
||||
if let Ok(claude_config_dir) = std::env::var("CLAUDE_CONFIG_DIR") {
|
||||
@@ -4987,7 +4987,14 @@ fn config_home_dir() -> Result<PathBuf, String> {
|
||||
if let Ok(path) = std::env::var("CLAW_CONFIG_HOME") {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
let home = std::env::var("HOME").map_err(|_| String::from("HOME is not set"))?;
|
||||
let home = std::env::var("HOME")
|
||||
.or_else(|_| std::env::var("USERPROFILE"))
|
||||
.map_err(|_| {
|
||||
String::from(
|
||||
"HOME is not set (on Windows, set USERPROFILE or HOME, \
|
||||
or use CLAW_CONFIG_HOME to point directly at the config directory)",
|
||||
)
|
||||
})?;
|
||||
Ok(PathBuf::from(home).join(".claw"))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user