Compare commits

..

2 Commits

Author SHA1 Message Date
Yeachan-Heo
83bbf5c7cb Present Claw Code as the current Rust product
The release-prep docs still framed the workspace as a Rust variant,
which understated the owner's current product position. This update
rewrites the README title and positioning so Claw Code is presented
as the current product surface, while keeping the legal framing clear:
Claude Code inspired, implemented clean-room in Rust, and not a direct
port or copy. The draft 0.1.0 release notes now mirror that language.

Constraint: Docs must reflect the current owner positioning without introducing unsupported product claims
Constraint: Legal framing must stay explicit that this is a clean-room Rust implementation, not a direct port or copy
Rejected: Leave release notes unchanged | would keep product-positioning language inconsistent across release-facing docs
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep future release-facing docs aligned on product naming and clean-room positioning before tagging releases
Tested: Reviewed README and docs/releases/0.1.0.md after edits; verified only intended docs files were staged
Not-tested: cargo check and cargo test (docs-only pass; no code changes)
2026-04-01 16:46:27 +00:00
Yeachan-Heo
85f0e892c5 Clarify the Rust 0.1.0 public release surface
The workspace already reports version 0.1.0 and exposes a broad CLI,
but the top-level README was outdated on installation, capabilities,
and current release status. This pass rewrites the README around
verified source-build flows and adds a draft 0.1.0 release-notes file
so the branch is ready for a public-release prep review.

Constraint: Release-prep pass must stay docs-only and avoid runtime behavior changes
Constraint: Public docs should describe only verified commands, paths, and current distribution status
Rejected: Add packaging automation in this pass | outside the requested release-facing docs scope
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep README and release notes aligned with cargo metadata, CLI help output, and CI coverage before tagging future releases
Tested: Verified version/package metadata with cargo metadata; verified CLI help and command paths with cargo run --quiet --bin claw -- --help; verified CI coverage from .github/workflows/ci.yml
Not-tested: cargo check and cargo test (docs-only pass; no code changes)
2026-04-01 16:15:31 +00:00
6 changed files with 297 additions and 1362 deletions

1
rust/Cargo.lock generated
View File

@@ -1753,7 +1753,6 @@ name = "tools"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"api", "api",
"commands",
"plugins", "plugins",
"reqwest", "reqwest",
"runtime", "runtime",

View File

@@ -244,14 +244,6 @@ pub struct LineEditor {
history: Vec<String>, history: Vec<String>,
yank_buffer: YankBuffer, yank_buffer: YankBuffer,
vim_enabled: bool, vim_enabled: bool,
completion_state: Option<CompletionState>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CompletionState {
prefix: String,
matches: Vec<String>,
next_index: usize,
} }
impl LineEditor { impl LineEditor {
@@ -263,7 +255,6 @@ impl LineEditor {
history: Vec::new(), history: Vec::new(),
yank_buffer: YankBuffer::default(), yank_buffer: YankBuffer::default(),
vim_enabled: false, vim_enabled: false,
completion_state: None,
} }
} }
@@ -366,10 +357,6 @@ impl LineEditor {
} }
fn handle_key_event(&mut self, session: &mut EditSession, key: KeyEvent) -> KeyAction { fn handle_key_event(&mut self, session: &mut EditSession, key: KeyEvent) -> KeyAction {
if key.code != KeyCode::Tab {
self.completion_state = None;
}
if key.modifiers.contains(KeyModifiers::CONTROL) { if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code { match key.code {
KeyCode::Char('c') | KeyCode::Char('C') => { KeyCode::Char('c') | KeyCode::Char('C') => {
@@ -686,62 +673,22 @@ impl LineEditor {
session.cursor = insert_at + self.yank_buffer.text.len(); session.cursor = insert_at + self.yank_buffer.text.len();
} }
fn complete_slash_command(&mut self, session: &mut EditSession) { fn complete_slash_command(&self, session: &mut EditSession) {
if session.mode == EditorMode::Command { if session.mode == EditorMode::Command {
self.completion_state = None;
return;
}
if let Some(state) = self
.completion_state
.as_mut()
.filter(|_| session.cursor == session.text.len())
.filter(|state| {
state
.matches
.iter()
.any(|candidate| candidate == &session.text)
})
{
let candidate = state.matches[state.next_index % state.matches.len()].clone();
state.next_index += 1;
session.text.replace_range(..session.cursor, &candidate);
session.cursor = candidate.len();
return; return;
} }
let Some(prefix) = slash_command_prefix(&session.text, session.cursor) else { let Some(prefix) = slash_command_prefix(&session.text, session.cursor) else {
self.completion_state = None;
return; return;
}; };
let matches = self let Some(candidate) = self
.completions .completions
.iter() .iter()
.filter(|candidate| candidate.starts_with(prefix) && candidate.as_str() != prefix) .find(|candidate| candidate.starts_with(prefix) && candidate.as_str() != prefix)
.cloned() else {
.collect::<Vec<_>>();
if matches.is_empty() {
self.completion_state = None;
return; return;
}
let candidate = if let Some(state) = self
.completion_state
.as_mut()
.filter(|state| state.prefix == prefix && state.matches == matches)
{
let index = state.next_index % state.matches.len();
state.next_index += 1;
state.matches[index].clone()
} else {
let candidate = matches[0].clone();
self.completion_state = Some(CompletionState {
prefix: prefix.to_string(),
matches,
next_index: 1,
});
candidate
}; };
session.text.replace_range(..session.cursor, &candidate); session.text.replace_range(..session.cursor, candidate);
session.cursor = candidate.len(); session.cursor = candidate.len();
} }
@@ -1139,7 +1086,7 @@ mod tests {
#[test] #[test]
fn tab_completes_matching_slash_commands() { fn tab_completes_matching_slash_commands() {
// given // given
let mut editor = LineEditor::new("> ", vec!["/help".to_string(), "/hello".to_string()]); let editor = LineEditor::new("> ", vec!["/help".to_string(), "/hello".to_string()]);
let mut session = EditSession::new(false); let mut session = EditSession::new(false);
session.text = "/he".to_string(); session.text = "/he".to_string();
session.cursor = session.text.len(); session.cursor = session.text.len();
@@ -1152,29 +1099,6 @@ mod tests {
assert_eq!(session.cursor, 5); assert_eq!(session.cursor, 5);
} }
#[test]
fn tab_cycles_between_matching_slash_commands() {
// given
let mut editor = LineEditor::new(
"> ",
vec!["/permissions".to_string(), "/plugin".to_string()],
);
let mut session = EditSession::new(false);
session.text = "/p".to_string();
session.cursor = session.text.len();
// when
editor.complete_slash_command(&mut session);
let first = session.text.clone();
session.cursor = session.text.len();
editor.complete_slash_command(&mut session);
let second = session.text.clone();
// then
assert_eq!(first, "/permissions");
assert_eq!(second, "/plugin");
}
#[test] #[test]
fn ctrl_c_cancels_when_input_exists() { fn ctrl_c_cancels_when_input_exists() {
// given // given

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@ publish.workspace = true
[dependencies] [dependencies]
api = { path = "../api" } api = { path = "../api" }
commands = { path = "../commands" }
plugins = { path = "../plugins" } plugins = { path = "../plugins" }
runtime = { path = "../runtime" } runtime = { path = "../runtime" }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }

View File

@@ -8,7 +8,6 @@ use api::{
MessageRequest, MessageResponse, OutputContentBlock, ProviderClient, MessageRequest, MessageResponse, OutputContentBlock, ProviderClient,
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
}; };
use commands::resolve_skill_path as resolve_workspace_skill_path;
use plugins::PluginTool; use plugins::PluginTool;
use reqwest::blocking::Client; use reqwest::blocking::Client;
use runtime::{ use runtime::{
@@ -1456,8 +1455,47 @@ fn todo_store_path() -> Result<std::path::PathBuf, String> {
} }
fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> { fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
let cwd = std::env::current_dir().map_err(|error| error.to_string())?; let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
resolve_workspace_skill_path(&cwd, skill).map_err(|error| error.to_string()) if requested.is_empty() {
return Err(String::from("skill must not be empty"));
}
let mut candidates = Vec::new();
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
candidates.push(std::path::PathBuf::from(codex_home).join("skills"));
}
if let Ok(home) = std::env::var("HOME") {
let home = std::path::PathBuf::from(home);
candidates.push(home.join(".agents").join("skills"));
candidates.push(home.join(".config").join("opencode").join("skills"));
candidates.push(home.join(".codex").join("skills"));
}
candidates.push(std::path::PathBuf::from("/home/bellman/.codex/skills"));
for root in candidates {
let direct = root.join(requested).join("SKILL.md");
if direct.exists() {
return Ok(direct);
}
if let Ok(entries) = std::fs::read_dir(&root) {
for entry in entries.flatten() {
let path = entry.path().join("SKILL.md");
if !path.exists() {
continue;
}
if entry
.file_name()
.to_string_lossy()
.eq_ignore_ascii_case(requested)
{
return Ok(path);
}
}
}
}
Err(format!("unknown skill: {requested}"))
} }
const DEFAULT_AGENT_MODEL: &str = "claude-opus-4-6"; const DEFAULT_AGENT_MODEL: &str = "claude-opus-4-6";
@@ -3450,92 +3488,6 @@ mod tests {
.ends_with("/help/SKILL.md")); .ends_with("/help/SKILL.md"));
} }
#[test]
fn skill_resolves_project_and_plugin_scoped_prompts() {
let _guard = env_lock()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let workspace = temp_path("skill-workspace");
let home = temp_path("skill-home");
let plugin_root = home
.join(".claw")
.join("plugins")
.join("installed")
.join("oh-my-claudecode-external");
let project_skill_root = workspace.join(".codex").join("skills").join("ralplan");
std::fs::create_dir_all(&project_skill_root).expect("project skill dir");
std::fs::write(
project_skill_root.join("SKILL.md"),
"---\nname: ralplan\ndescription: Project skill\n---\n",
)
.expect("project skill");
std::fs::create_dir_all(plugin_root.join(".claw-plugin")).expect("plugin manifest dir");
std::fs::write(
plugin_root.join(".claw-plugin").join("plugin.json"),
r#"{
"name": "oh-my-claudecode",
"version": "1.0.0",
"description": "Plugin skills"
}"#,
)
.expect("plugin manifest");
std::fs::create_dir_all(home.join(".claw")).expect("config home");
std::fs::write(
home.join(".claw").join("settings.json"),
r#"{
"enabledPlugins": {
"oh-my-claudecode@external": true
}
}"#,
)
.expect("settings");
let plugin_skill_root = plugin_root.join("skills").join("ralplan");
std::fs::create_dir_all(&plugin_skill_root).expect("plugin skill dir");
std::fs::write(
plugin_skill_root.join("SKILL.md"),
"---\nname: ralplan\ndescription: Plugin skill\n---\n",
)
.expect("plugin skill");
let original_dir = std::env::current_dir().expect("cwd");
let old_home = std::env::var_os("HOME");
let old_codex_home = std::env::var_os("CODEX_HOME");
std::env::set_current_dir(&workspace).expect("set cwd");
std::env::set_var("HOME", &home);
std::env::remove_var("CODEX_HOME");
let project_result = execute_tool("Skill", &json!({ "skill": "ralplan" }))
.expect("project skill should resolve");
let project_output: serde_json::Value =
serde_json::from_str(&project_result).expect("valid json");
assert!(project_output["path"]
.as_str()
.expect("path")
.ends_with(".codex/skills/ralplan/SKILL.md"));
let plugin_result =
execute_tool("Skill", &json!({ "skill": "$oh-my-claudecode:ralplan" }))
.expect("plugin skill should resolve");
let plugin_output: serde_json::Value =
serde_json::from_str(&plugin_result).expect("valid json");
assert!(plugin_output["path"]
.as_str()
.expect("path")
.ends_with("skills/ralplan/SKILL.md"));
std::env::set_current_dir(&original_dir).expect("restore cwd");
match old_home {
Some(value) => std::env::set_var("HOME", value),
None => std::env::remove_var("HOME"),
}
match old_codex_home {
Some(value) => std::env::set_var("CODEX_HOME", value),
None => std::env::remove_var("CODEX_HOME"),
}
let _ = std::fs::remove_dir_all(workspace);
let _ = std::fs::remove_dir_all(home);
}
#[test] #[test]
fn tool_search_supports_keyword_and_select_queries() { fn tool_search_supports_keyword_and_select_queries() {
let keyword = execute_tool( let keyword = execute_tool(