mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-04 18:44:49 +08:00
Compare commits
4 Commits
feat/uiux-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7030d26e7a | ||
|
|
cf0047207f | ||
|
|
16c6d23e19 | ||
|
|
8947e382e1 |
38
README.md
38
README.md
@@ -33,38 +33,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Built with oh-my-opencode
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/code-yeongyu/oh-my-openagent">
|
|
||||||
<img src="https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/.github/assets/omo.png" width="600" />
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/code-yeongyu/oh-my-openagent"><strong>oh-my-opencode</strong></a> — the agent orchestration layer that makes AI coding actually work.
|
|
||||||
<br />
|
|
||||||
<em>Sisyphus doesn't stop until the task is done. Every test passes. Every review clears.</em>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/code-yeongyu/oh-my-openagent"><img src="https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=for-the-badge&logo=github" /></a>
|
|
||||||
<a href="https://www.npmjs.com/package/oh-my-opencode"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fohmyopenagent.com%2Fapi%2Fnpm-downloads&style=for-the-badge" /></a>
|
|
||||||
<a href="https://discord.gg/PUwSMR9XNk"><img src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge" /></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
The **entire Rust port** was built by oh-my-opencode's **Sisyphus** agent in `ultrawork` mode.
|
|
||||||
|
|
||||||
> *"If Claude Code does in 7 days what a human does in 3 months, Sisyphus does it in 1 hour."* — B, Quant Researcher
|
|
||||||
|
|
||||||
> *"Oh My OpenCode Is Actually Insane"* — [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
|
|
||||||
|
|
||||||
**Credits:** [@code-yeongyu](https://github.com/code-yeongyu) (oh-my-opencode creator) · **Sisyphus** (autonomous coding agent) · **Jobdori**
|
|
||||||
|
|
||||||
<p align="center"><code>npx oh-my-opencode@latest</code></p>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rust Port
|
## Rust Port
|
||||||
|
|
||||||
The Rust workspace under `rust/` is the current systems-language port of the project.
|
The Rust workspace under `rust/` is the current systems-language port of the project.
|
||||||
@@ -94,7 +62,7 @@ The whole thing was orchestrated end-to-end using [oh-my-codex (OmX)](https://gi
|
|||||||
|
|
||||||
The result is a clean-room Python rewrite that captures the architectural patterns of Claw Code's agent harness without copying any proprietary source. I'm now actively collaborating with [@bellman_ych](https://x.com/bellman_ych) — the creator of OmX himself — to push this further. The basic Python foundation is already in place and functional, but we're just getting started. **Stay tuned — a much more capable version is on the way.**
|
The result is a clean-room Python rewrite that captures the architectural patterns of Claw Code's agent harness without copying any proprietary source. I'm now actively collaborating with [@bellman_ych](https://x.com/bellman_ych) — the creator of OmX himself — to push this further. The basic Python foundation is already in place and functional, but we're just getting started. **Stay tuned — a much more capable version is on the way.**
|
||||||
|
|
||||||
The Rust port was built separately using [oh-my-opencode (OMO)](https://github.com/code-yeongyu/oh-my-opencode) by [@q_yeon_gyu_kim](https://x.com/q_yeon_gyu_kim) ([@code-yeongyu](https://github.com/code-yeongyu)), which orchestrates [opencode](https://opencode.ai) agents. **The scaffolding and architecture direction were established with [oh-my-codex (OmX)](https://github.com/Yeachan-Heo/oh-my-codex),** and the **Sisyphus** agent then handled implementation work across the API client, runtime engine, CLI, plugin system, MCP integration, and the cleanroom pass in `ultrawork` mode.
|
The Rust port was developed with both [oh-my-codex (OmX)](https://github.com/Yeachan-Heo/oh-my-codex) and [oh-my-opencode (OmO)](https://github.com/code-yeongyu/oh-my-openagent): OmX drove scaffolding, orchestration, and architecture direction, while OmO was used for later implementation acceleration and verification support.
|
||||||
|
|
||||||
https://github.com/instructkr/claw-code
|
https://github.com/instructkr/claw-code
|
||||||
|
|
||||||
@@ -220,8 +188,8 @@ The port now mirrors the archived root-entry file surface, top-level subsystem n
|
|||||||
|
|
||||||
This repository's porting, cleanroom hardening, and verification workflow was AI-assisted with Yeachan Heo's tooling stack, with **oh-my-codex (OmX)** as the primary scaffolding and orchestration layer.
|
This repository's porting, cleanroom hardening, and verification workflow was AI-assisted with Yeachan Heo's tooling stack, with **oh-my-codex (OmX)** as the primary scaffolding and orchestration layer.
|
||||||
|
|
||||||
- [**oh-my-codex (OmX)**](https://github.com/Yeachan-Heo/oh-my-codex) — main branch credit: primary scaffolding, orchestration, and core porting workflow
|
- [**oh-my-codex (OmX)**](https://github.com/Yeachan-Heo/oh-my-codex) — scaffolding, orchestration, architecture direction, and core porting workflow
|
||||||
- [**oh-my-opencode (OmO)**](https://github.com/instructkr/oh-my-opencode) — implementation acceleration, cleanup passes, and verification support
|
- [**oh-my-opencode (OmO)**](https://github.com/code-yeongyu/oh-my-openagent) — implementation acceleration, cleanup, and verification support
|
||||||
|
|
||||||
Key workflow patterns used during the port:
|
Key workflow patterns used during the port:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::collections::BTreeSet;
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, IsTerminal, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -16,15 +16,14 @@ use std::thread;
|
|||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use api::{
|
use api::{
|
||||||
resolve_startup_auth_source, AuthSource, ClawApiClient, ContentBlockDelta, InputContentBlock,
|
resolve_startup_auth_source, ClawApiClient, AuthSource, ContentBlockDelta, InputContentBlock,
|
||||||
InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
|
InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
|
||||||
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command,
|
handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command,
|
||||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
render_slash_command_help, resume_supported_slash_commands, slash_command_specs, SlashCommand,
|
||||||
suggest_slash_commands, SlashCommand,
|
|
||||||
};
|
};
|
||||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||||
use init::initialize_repo;
|
use init::initialize_repo;
|
||||||
@@ -60,25 +59,15 @@ type AllowedToolSet = BTreeSet<String>;
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(error) = run() {
|
if let Err(error) = run() {
|
||||||
eprintln!("{}", render_cli_error(&error.to_string()));
|
eprintln!(
|
||||||
|
"error: {error}
|
||||||
|
|
||||||
|
Run `claw --help` for usage."
|
||||||
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_cli_error(problem: &str) -> String {
|
|
||||||
let mut lines = vec!["Error".to_string()];
|
|
||||||
for (index, line) in problem.lines().enumerate() {
|
|
||||||
let label = if index == 0 {
|
|
||||||
" Problem "
|
|
||||||
} else {
|
|
||||||
" "
|
|
||||||
};
|
|
||||||
lines.push(format!("{label}{line}"));
|
|
||||||
}
|
|
||||||
lines.push(" Help claw --help".to_string());
|
|
||||||
lines.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args: Vec<String> = env::args().skip(1).collect();
|
let args: Vec<String> = env::args().skip(1).collect();
|
||||||
match parse_args(&args)? {
|
match parse_args(&args)? {
|
||||||
@@ -332,36 +321,17 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
|||||||
Some(SlashCommand::Help) => Ok(CliAction::Help),
|
Some(SlashCommand::Help) => Ok(CliAction::Help),
|
||||||
Some(SlashCommand::Agents { args }) => Ok(CliAction::Agents { args }),
|
Some(SlashCommand::Agents { args }) => Ok(CliAction::Agents { args }),
|
||||||
Some(SlashCommand::Skills { args }) => Ok(CliAction::Skills { args }),
|
Some(SlashCommand::Skills { args }) => Ok(CliAction::Skills { args }),
|
||||||
Some(command) => Err(format_direct_slash_command_error(
|
Some(command) => Err(format!(
|
||||||
match &command {
|
"unsupported direct slash command outside the REPL: {command_name}",
|
||||||
|
command_name = match command {
|
||||||
SlashCommand::Unknown(name) => format!("/{name}"),
|
SlashCommand::Unknown(name) => format!("/{name}"),
|
||||||
_ => rest[0].clone(),
|
_ => rest[0].clone(),
|
||||||
}
|
}
|
||||||
.as_str(),
|
|
||||||
matches!(command, SlashCommand::Unknown(_)),
|
|
||||||
)),
|
)),
|
||||||
None => Err(format!("unknown subcommand: {}", rest[0])),
|
None => Err(format!("unknown subcommand: {}", rest[0])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_direct_slash_command_error(command: &str, is_unknown: bool) -> String {
|
|
||||||
let trimmed = command.trim().trim_start_matches('/');
|
|
||||||
let mut lines = vec![
|
|
||||||
"Direct slash command unavailable".to_string(),
|
|
||||||
format!(" Command /{trimmed}"),
|
|
||||||
];
|
|
||||||
if is_unknown {
|
|
||||||
append_slash_command_suggestions(&mut lines, trimmed);
|
|
||||||
} else {
|
|
||||||
lines.push(" Try Start `claw` to use interactive slash commands".to_string());
|
|
||||||
lines.push(
|
|
||||||
" Tip Resume-safe commands also work with `claw --resume SESSION.json ...`"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
lines.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_model_alias(model: &str) -> &str {
|
fn resolve_model_alias(model: &str) -> &str {
|
||||||
match model {
|
match model {
|
||||||
"opus" => "claude-opus-4-6",
|
"opus" => "claude-opus-4-6",
|
||||||
@@ -700,17 +670,13 @@ struct StatusUsage {
|
|||||||
fn format_model_report(model: &str, message_count: usize, turns: u32) -> String {
|
fn format_model_report(model: &str, message_count: usize, turns: u32) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Model
|
"Model
|
||||||
Current {model}
|
Current model {model}
|
||||||
Session {message_count} messages · {turns} turns
|
Session messages {message_count}
|
||||||
|
Session turns {turns}
|
||||||
|
|
||||||
Aliases
|
Usage
|
||||||
opus claude-opus-4-6
|
Inspect current model with /model
|
||||||
sonnet claude-sonnet-4-6
|
Switch models with /model <name>"
|
||||||
haiku claude-haiku-4-5-20251213
|
|
||||||
|
|
||||||
Next
|
|
||||||
/model Show the current model
|
|
||||||
/model <name> Switch models for this REPL session"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,8 +685,7 @@ fn format_model_switch_report(previous: &str, next: &str, message_count: usize)
|
|||||||
"Model updated
|
"Model updated
|
||||||
Previous {previous}
|
Previous {previous}
|
||||||
Current {next}
|
Current {next}
|
||||||
Preserved {message_count} messages
|
Preserved msgs {message_count}"
|
||||||
Tip Existing conversation context stayed attached"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,34 +718,28 @@ fn format_permissions_report(mode: &str) -> String {
|
|||||||
",
|
",
|
||||||
);
|
);
|
||||||
|
|
||||||
let effect = match mode {
|
|
||||||
"read-only" => "Only read/search tools can run automatically",
|
|
||||||
"workspace-write" => "Editing tools can modify files in the workspace",
|
|
||||||
"danger-full-access" => "All tools can run without additional sandbox limits",
|
|
||||||
_ => "Unknown permission mode",
|
|
||||||
};
|
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"Permissions
|
"Permissions
|
||||||
Active mode {mode}
|
Active mode {mode}
|
||||||
Effect {effect}
|
Mode status live session default
|
||||||
|
|
||||||
Modes
|
Modes
|
||||||
{modes}
|
{modes}
|
||||||
|
|
||||||
Next
|
Usage
|
||||||
/permissions Show the current mode
|
Inspect current mode with /permissions
|
||||||
/permissions <mode> Switch modes for subsequent tool calls"
|
Switch modes with /permissions <mode>"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_permissions_switch_report(previous: &str, next: &str) -> String {
|
fn format_permissions_switch_report(previous: &str, next: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"Permissions updated
|
"Permissions updated
|
||||||
|
Result mode switched
|
||||||
Previous mode {previous}
|
Previous mode {previous}
|
||||||
Active mode {next}
|
Active mode {next}
|
||||||
Applies to Subsequent tool calls in this REPL
|
Applies to subsequent tool calls
|
||||||
Tip Run /permissions to review all available modes"
|
Usage /permissions to inspect current mode"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,11 +750,7 @@ fn format_cost_report(usage: TokenUsage) -> String {
|
|||||||
Output tokens {}
|
Output tokens {}
|
||||||
Cache create {}
|
Cache create {}
|
||||||
Cache read {}
|
Cache read {}
|
||||||
Total tokens {}
|
Total tokens {}",
|
||||||
|
|
||||||
Next
|
|
||||||
/status See session + workspace context
|
|
||||||
/compact Trim local history if the session is getting large",
|
|
||||||
usage.input_tokens,
|
usage.input_tokens,
|
||||||
usage.output_tokens,
|
usage.output_tokens,
|
||||||
usage.cache_creation_input_tokens,
|
usage.cache_creation_input_tokens,
|
||||||
@@ -808,8 +763,8 @@ fn format_resume_report(session_path: &str, message_count: usize, turns: u32) ->
|
|||||||
format!(
|
format!(
|
||||||
"Session resumed
|
"Session resumed
|
||||||
Session file {session_path}
|
Session file {session_path}
|
||||||
History {message_count} messages · {turns} turns
|
Messages {message_count}
|
||||||
Next /status · /diff · /export"
|
Turns {turns}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,7 +773,7 @@ fn format_compact_report(removed: usize, resulting_messages: usize, skipped: boo
|
|||||||
format!(
|
format!(
|
||||||
"Compact
|
"Compact
|
||||||
Result skipped
|
Result skipped
|
||||||
Reason Session is already below the compaction threshold
|
Reason session below compaction threshold
|
||||||
Messages kept {resulting_messages}"
|
Messages kept {resulting_messages}"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -826,8 +781,7 @@ fn format_compact_report(removed: usize, resulting_messages: usize, skipped: boo
|
|||||||
"Compact
|
"Compact
|
||||||
Result compacted
|
Result compacted
|
||||||
Messages removed {removed}
|
Messages removed {removed}
|
||||||
Messages kept {resulting_messages}
|
Messages kept {resulting_messages}"
|
||||||
Tip Use /status to review the trimmed session"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1098,65 +1052,28 @@ impl LiveCli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn startup_banner(&self) -> String {
|
fn startup_banner(&self) -> String {
|
||||||
let color = io::stdout().is_terminal();
|
let cwd = env::current_dir().map_or_else(
|
||||||
let cwd = env::current_dir().ok();
|
|_| "<unknown>".to_string(),
|
||||||
let cwd_display = cwd.as_ref().map_or_else(
|
|
||||||
|| "<unknown>".to_string(),
|
|
||||||
|path| path.display().to_string(),
|
|path| path.display().to_string(),
|
||||||
);
|
);
|
||||||
let workspace_name = cwd
|
format!(
|
||||||
.as_ref()
|
"\x1b[38;5;196m\
|
||||||
.and_then(|path| path.file_name())
|
██████╗██╗ █████╗ ██╗ ██╗\n\
|
||||||
.and_then(|name| name.to_str())
|
██╔════╝██║ ██╔══██╗██║ ██║\n\
|
||||||
.unwrap_or("workspace");
|
██║ ██║ ███████║██║ █╗ ██║\n\
|
||||||
let git_branch = status_context(Some(&self.session.path))
|
██║ ██║ ██╔══██║██║███╗██║\n\
|
||||||
.ok()
|
╚██████╗███████╗██║ ██║╚███╔███╔╝\n\
|
||||||
.and_then(|context| context.git_branch);
|
╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\x1b[0m \x1b[38;5;208mCode\x1b[0m 🦞\n\n\
|
||||||
let workspace_summary = git_branch.as_deref().map_or_else(
|
\x1b[2mModel\x1b[0m {}\n\
|
||||||
|| workspace_name.to_string(),
|
\x1b[2mPermissions\x1b[0m {}\n\
|
||||||
|branch| format!("{workspace_name} · {branch}"),
|
\x1b[2mDirectory\x1b[0m {}\n\
|
||||||
);
|
\x1b[2mSession\x1b[0m {}\n\n\
|
||||||
let has_claw_md = cwd
|
Type \x1b[1m/help\x1b[0m for commands · \x1b[2mShift+Enter\x1b[0m for newline",
|
||||||
.as_ref()
|
self.model,
|
||||||
.is_some_and(|path| path.join("CLAW.md").is_file());
|
self.permission_mode.as_str(),
|
||||||
let mut lines = vec![
|
cwd,
|
||||||
format!(
|
self.session.id,
|
||||||
"{} {}",
|
)
|
||||||
if color {
|
|
||||||
"\x1b[1;38;5;45m🦞 Claw Code\x1b[0m"
|
|
||||||
} else {
|
|
||||||
"Claw Code"
|
|
||||||
},
|
|
||||||
if color {
|
|
||||||
"\x1b[2m· ready\x1b[0m"
|
|
||||||
} else {
|
|
||||||
"· ready"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
format!(" Workspace {workspace_summary}"),
|
|
||||||
format!(" Directory {cwd_display}"),
|
|
||||||
format!(" Model {}", self.model),
|
|
||||||
format!(" Permissions {}", self.permission_mode.as_str()),
|
|
||||||
format!(" Session {}", self.session.id),
|
|
||||||
format!(
|
|
||||||
" Quick start {}",
|
|
||||||
if has_claw_md {
|
|
||||||
"/help · /status · ask for a task"
|
|
||||||
} else {
|
|
||||||
"/init · /help · /status"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
" Editor Tab completes slash commands · /vim toggles modal editing"
|
|
||||||
.to_string(),
|
|
||||||
" Multiline Shift+Enter or Ctrl+J inserts a newline".to_string(),
|
|
||||||
];
|
|
||||||
if !has_claw_md {
|
|
||||||
lines.push(
|
|
||||||
" First run /init scaffolds CLAW.md, .claw.json, and local session files"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
lines.join("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -1329,28 +1246,19 @@ impl LiveCli {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Branch { .. } => {
|
SlashCommand::Branch { .. } => {
|
||||||
eprintln!(
|
eprintln!("git branch commands not yet wired to REPL");
|
||||||
"{}",
|
|
||||||
render_mode_unavailable("branch", "git branch commands")
|
|
||||||
);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Worktree { .. } => {
|
SlashCommand::Worktree { .. } => {
|
||||||
eprintln!(
|
eprintln!("git worktree commands not yet wired to REPL");
|
||||||
"{}",
|
|
||||||
render_mode_unavailable("worktree", "git worktree commands")
|
|
||||||
);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::CommitPushPr { .. } => {
|
SlashCommand::CommitPushPr { .. } => {
|
||||||
eprintln!(
|
eprintln!("commit-push-pr not yet wired to REPL");
|
||||||
"{}",
|
|
||||||
render_mode_unavailable("commit-push-pr", "commit + push + PR automation")
|
|
||||||
);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Unknown(name) => {
|
SlashCommand::Unknown(name) => {
|
||||||
eprintln!("{}", render_unknown_slash_command(&name));
|
eprintln!("unknown slash command: /{name}");
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1929,20 +1837,6 @@ fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::er
|
|||||||
Ok(sessions)
|
Ok(sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_relative_timestamp(epoch_secs: u64) -> String {
|
|
||||||
let now = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|duration| duration.as_secs())
|
|
||||||
.unwrap_or(epoch_secs);
|
|
||||||
let elapsed = now.saturating_sub(epoch_secs);
|
|
||||||
match elapsed {
|
|
||||||
0..=59 => format!("{elapsed}s ago"),
|
|
||||||
60..=3_599 => format!("{}m ago", elapsed / 60),
|
|
||||||
3_600..=86_399 => format!("{}h ago", elapsed / 3_600),
|
|
||||||
_ => format!("{}d ago", elapsed / 86_400),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::error::Error>> {
|
fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let sessions = list_managed_sessions()?;
|
let sessions = list_managed_sessions()?;
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
@@ -1960,28 +1854,26 @@ fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::e
|
|||||||
"○ saved"
|
"○ saved"
|
||||||
};
|
};
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
" {id:<20} {marker:<10} {msgs:>3} msgs · updated {modified}",
|
" {id:<20} {marker:<10} msgs={msgs:<4} modified={modified} path={path}",
|
||||||
id = session.id,
|
id = session.id,
|
||||||
msgs = session.message_count,
|
msgs = session.message_count,
|
||||||
modified = format_relative_timestamp(session.modified_epoch_secs),
|
modified = session.modified_epoch_secs,
|
||||||
|
path = session.path.display(),
|
||||||
));
|
));
|
||||||
lines.push(format!(" {}", session.path.display()));
|
|
||||||
}
|
}
|
||||||
Ok(lines.join("\n"))
|
Ok(lines.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_repl_help() -> String {
|
fn render_repl_help() -> String {
|
||||||
[
|
[
|
||||||
"Interactive REPL".to_string(),
|
"REPL".to_string(),
|
||||||
" Quick start Ask a task in plain English or use one of the core commands below."
|
" /exit Quit the REPL".to_string(),
|
||||||
.to_string(),
|
" /quit Quit the REPL".to_string(),
|
||||||
" Core commands /help · /status · /model · /permissions · /compact".to_string(),
|
" /vim Toggle Vim keybindings".to_string(),
|
||||||
" Exit /exit or /quit".to_string(),
|
" Up/Down Navigate prompt history".to_string(),
|
||||||
" Vim mode /vim toggles modal editing".to_string(),
|
" Tab Complete slash commands".to_string(),
|
||||||
" History Up/Down recalls previous prompts".to_string(),
|
" Ctrl-C Clear input (or exit on empty prompt)".to_string(),
|
||||||
" Completion Tab cycles slash command matches".to_string(),
|
" Shift+Enter/Ctrl+J Insert a newline".to_string(),
|
||||||
" Cancel Ctrl-C clears input (or exits on an empty prompt)".to_string(),
|
|
||||||
" Multiline Shift+Enter or Ctrl+J inserts a newline".to_string(),
|
|
||||||
String::new(),
|
String::new(),
|
||||||
render_slash_command_help(),
|
render_slash_command_help(),
|
||||||
]
|
]
|
||||||
@@ -1991,41 +1883,6 @@ fn render_repl_help() -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append_slash_command_suggestions(lines: &mut Vec<String>, name: &str) {
|
|
||||||
let suggestions = suggest_slash_commands(name, 3);
|
|
||||||
if suggestions.is_empty() {
|
|
||||||
lines.push(" Try /help shows the full slash command map".to_string());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(" Try /help shows the full slash command map".to_string());
|
|
||||||
lines.push("Suggestions".to_string());
|
|
||||||
lines.extend(
|
|
||||||
suggestions
|
|
||||||
.into_iter()
|
|
||||||
.map(|suggestion| format!(" {suggestion}")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_unknown_slash_command(name: &str) -> String {
|
|
||||||
let mut lines = vec![
|
|
||||||
"Unknown slash command".to_string(),
|
|
||||||
format!(" Command /{name}"),
|
|
||||||
];
|
|
||||||
append_slash_command_suggestions(&mut lines, name);
|
|
||||||
lines.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_mode_unavailable(command: &str, label: &str) -> String {
|
|
||||||
[
|
|
||||||
"Command unavailable in this REPL mode".to_string(),
|
|
||||||
format!(" Command /{command}"),
|
|
||||||
format!(" Feature {label}"),
|
|
||||||
" Tip Use /help to find currently wired REPL commands".to_string(),
|
|
||||||
]
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status_context(
|
fn status_context(
|
||||||
session_path: Option<&Path>,
|
session_path: Option<&Path>,
|
||||||
) -> Result<StatusContext, Box<dyn std::error::Error>> {
|
) -> Result<StatusContext, Box<dyn std::error::Error>> {
|
||||||
@@ -2055,41 +1912,33 @@ fn format_status_report(
|
|||||||
) -> String {
|
) -> String {
|
||||||
[
|
[
|
||||||
format!(
|
format!(
|
||||||
"Session
|
"Status
|
||||||
Model {model}
|
Model {model}
|
||||||
Permissions {permission_mode}
|
Permission mode {permission_mode}
|
||||||
Activity {} messages · {} turns
|
Messages {}
|
||||||
Tokens est {} · latest {} · total {}",
|
Turns {}
|
||||||
usage.message_count,
|
Estimated tokens {}",
|
||||||
usage.turns,
|
usage.message_count, usage.turns, usage.estimated_tokens,
|
||||||
usage.estimated_tokens,
|
|
||||||
usage.latest.total_tokens(),
|
|
||||||
usage.cumulative.total_tokens(),
|
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
"Usage
|
"Usage
|
||||||
|
Latest total {}
|
||||||
Cumulative input {}
|
Cumulative input {}
|
||||||
Cumulative output {}
|
Cumulative output {}
|
||||||
Cache create {}
|
Cumulative total {}",
|
||||||
Cache read {}",
|
usage.latest.total_tokens(),
|
||||||
usage.cumulative.input_tokens,
|
usage.cumulative.input_tokens,
|
||||||
usage.cumulative.output_tokens,
|
usage.cumulative.output_tokens,
|
||||||
usage.cumulative.cache_creation_input_tokens,
|
usage.cumulative.total_tokens(),
|
||||||
usage.cumulative.cache_read_input_tokens,
|
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
"Workspace
|
"Workspace
|
||||||
Folder {}
|
Cwd {}
|
||||||
Project root {}
|
Project root {}
|
||||||
Git branch {}
|
Git branch {}
|
||||||
Session file {}
|
Session {}
|
||||||
Config files loaded {}/{}
|
Config files loaded {}/{}
|
||||||
Memory files {}
|
Memory files {}",
|
||||||
|
|
||||||
Next
|
|
||||||
/help Browse commands
|
|
||||||
/session list Inspect saved sessions
|
|
||||||
/diff Review current workspace changes",
|
|
||||||
context.cwd.display(),
|
context.cwd.display(),
|
||||||
context
|
context
|
||||||
.project_root
|
.project_root
|
||||||
@@ -2204,7 +2053,8 @@ fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
|
|||||||
if project_context.instruction_files.is_empty() {
|
if project_context.instruction_files.is_empty() {
|
||||||
lines.push("Discovered files".to_string());
|
lines.push("Discovered files".to_string());
|
||||||
lines.push(
|
lines.push(
|
||||||
" No CLAW instruction files discovered in the current directory ancestry.".to_string(),
|
" No CLAW instruction files discovered in the current directory ancestry."
|
||||||
|
.to_string(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
lines.push("Discovered files".to_string());
|
lines.push("Discovered files".to_string());
|
||||||
@@ -2471,7 +2321,7 @@ fn render_version_report() -> String {
|
|||||||
let git_sha = GIT_SHA.unwrap_or("unknown");
|
let git_sha = GIT_SHA.unwrap_or("unknown");
|
||||||
let target = BUILD_TARGET.unwrap_or("unknown");
|
let target = BUILD_TARGET.unwrap_or("unknown");
|
||||||
format!(
|
format!(
|
||||||
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Target {target}\n Build date {DEFAULT_DATE}\n\nSupport\n Help claw --help\n REPL /help"
|
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Target {target}\n Build date {DEFAULT_DATE}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2956,8 +2806,7 @@ fn build_runtime(
|
|||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
progress_reporter: Option<InternalPromptProgressReporter>,
|
progress_reporter: Option<InternalPromptProgressReporter>,
|
||||||
) -> Result<ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
) -> Result<ConversationRuntime<DefaultRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>> {
|
||||||
{
|
|
||||||
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
let (feature_config, tool_registry) = build_runtime_plugin_state()?;
|
||||||
Ok(ConversationRuntime::new_with_features(
|
Ok(ConversationRuntime::new_with_features(
|
||||||
session,
|
session,
|
||||||
@@ -3881,118 +3730,65 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||||
writeln!(out, "Claw Code CLI v{VERSION}")?;
|
writeln!(out, "claw v{VERSION}")?;
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" Interactive coding assistant for the current workspace."
|
|
||||||
)?;
|
|
||||||
writeln!(out)?;
|
writeln!(out)?;
|
||||||
writeln!(out, "Quick start")?;
|
writeln!(out, "Usage:")?;
|
||||||
writeln!(
|
writeln!(
|
||||||
out,
|
out,
|
||||||
" claw Start the interactive REPL"
|
" claw [--model MODEL] [--allowedTools TOOL[,TOOL...]]"
|
||||||
|
)?;
|
||||||
|
writeln!(out, " Start the interactive REPL")?;
|
||||||
|
writeln!(
|
||||||
|
out,
|
||||||
|
" claw [--model MODEL] [--output-format text|json] prompt TEXT"
|
||||||
|
)?;
|
||||||
|
writeln!(out, " Send one prompt and exit")?;
|
||||||
|
writeln!(
|
||||||
|
out,
|
||||||
|
" claw [--model MODEL] [--output-format text|json] TEXT"
|
||||||
|
)?;
|
||||||
|
writeln!(out, " Shorthand non-interactive prompt mode")?;
|
||||||
|
writeln!(
|
||||||
|
out,
|
||||||
|
" claw --resume SESSION.json [/status] [/compact] [...]"
|
||||||
)?;
|
)?;
|
||||||
writeln!(
|
writeln!(
|
||||||
out,
|
out,
|
||||||
" claw \"summarize this repo\" Run one prompt and exit"
|
" Inspect or maintain a saved session without entering the REPL"
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" claw prompt \"explain src/main.rs\" Explicit one-shot prompt"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" claw --resume SESSION.json /status Inspect a saved session"
|
|
||||||
)?;
|
|
||||||
writeln!(out)?;
|
|
||||||
writeln!(out, "Interactive essentials")?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" /help Browse the full slash command map"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" /status Inspect session + workspace state"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" /model <name> Switch models mid-session"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" /permissions <mode> Adjust tool access"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" Tab Complete slash commands"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" /vim Toggle modal editing"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" Shift+Enter / Ctrl+J Insert a newline"
|
|
||||||
)?;
|
|
||||||
writeln!(out)?;
|
|
||||||
writeln!(out, "Commands")?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" claw dump-manifests Read upstream TS sources and print extracted counts"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" claw bootstrap-plan Print the bootstrap phase skeleton"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" claw agents List configured agents"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" claw skills List installed skills"
|
|
||||||
)?;
|
)?;
|
||||||
|
writeln!(out, " claw dump-manifests")?;
|
||||||
|
writeln!(out, " claw bootstrap-plan")?;
|
||||||
|
writeln!(out, " claw agents")?;
|
||||||
|
writeln!(out, " claw skills")?;
|
||||||
writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
|
writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
|
||||||
|
writeln!(out, " claw login")?;
|
||||||
|
writeln!(out, " claw logout")?;
|
||||||
|
writeln!(out, " claw init")?;
|
||||||
|
writeln!(out)?;
|
||||||
|
writeln!(out, "Flags:")?;
|
||||||
writeln!(
|
writeln!(
|
||||||
out,
|
out,
|
||||||
" claw login Start the OAuth login flow"
|
" --model MODEL Override the active model"
|
||||||
)?;
|
)?;
|
||||||
writeln!(
|
writeln!(
|
||||||
out,
|
out,
|
||||||
" claw logout Clear saved OAuth credentials"
|
" --output-format FORMAT Non-interactive output format: text or json"
|
||||||
)?;
|
)?;
|
||||||
writeln!(
|
writeln!(
|
||||||
out,
|
out,
|
||||||
" claw init Scaffold CLAW.md + local files"
|
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
out,
|
||||||
|
" --dangerously-skip-permissions Skip all permission checks"
|
||||||
|
)?;
|
||||||
|
writeln!(out, " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)")?;
|
||||||
|
writeln!(
|
||||||
|
out,
|
||||||
|
" --version, -V Print version and build information locally"
|
||||||
)?;
|
)?;
|
||||||
writeln!(out)?;
|
writeln!(out)?;
|
||||||
writeln!(out, "Flags")?;
|
writeln!(out, "Interactive slash commands:")?;
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" --model MODEL Override the active model"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" --output-format FORMAT Non-interactive output: text or json"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" --dangerously-skip-permissions Skip all permission checks"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
out,
|
|
||||||
" --version, -V Print version and build information"
|
|
||||||
)?;
|
|
||||||
writeln!(out)?;
|
|
||||||
writeln!(out, "Slash command reference")?;
|
|
||||||
writeln!(out, "{}", render_slash_command_help())?;
|
writeln!(out, "{}", render_slash_command_help())?;
|
||||||
writeln!(out)?;
|
writeln!(out)?;
|
||||||
let resume_commands = resume_supported_slash_commands()
|
let resume_commands = resume_supported_slash_commands()
|
||||||
@@ -4004,7 +3800,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
writeln!(out, "Resume-safe commands: {resume_commands}")?;
|
writeln!(out, "Resume-safe commands: {resume_commands}")?;
|
||||||
writeln!(out, "Examples")?;
|
writeln!(out, "Examples:")?;
|
||||||
writeln!(out, " claw --model opus \"summarize this repo\"")?;
|
writeln!(out, " claw --model opus \"summarize this repo\"")?;
|
||||||
writeln!(
|
writeln!(
|
||||||
out,
|
out,
|
||||||
@@ -4276,8 +4072,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let error = parse_args(&["/status".to_string()])
|
let error = parse_args(&["/status".to_string()])
|
||||||
.expect_err("/status should remain REPL-only when invoked directly");
|
.expect_err("/status should remain REPL-only when invoked directly");
|
||||||
assert!(error.contains("Direct slash command unavailable"));
|
assert!(error.contains("unsupported direct slash command"));
|
||||||
assert!(error.contains("/status"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4354,14 +4149,13 @@ mod tests {
|
|||||||
fn shared_help_uses_resume_annotation_copy() {
|
fn shared_help_uses_resume_annotation_copy() {
|
||||||
let help = commands::render_slash_command_help();
|
let help = commands::render_slash_command_help();
|
||||||
assert!(help.contains("Slash commands"));
|
assert!(help.contains("Slash commands"));
|
||||||
assert!(help.contains("Tab completes commands inside the REPL."));
|
|
||||||
assert!(help.contains("works with --resume SESSION.json"));
|
assert!(help.contains("works with --resume SESSION.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_help_includes_shared_commands_and_exit() {
|
fn repl_help_includes_shared_commands_and_exit() {
|
||||||
let help = render_repl_help();
|
let help = render_repl_help();
|
||||||
assert!(help.contains("Interactive REPL"));
|
assert!(help.contains("REPL"));
|
||||||
assert!(help.contains("/help"));
|
assert!(help.contains("/help"));
|
||||||
assert!(help.contains("/status"));
|
assert!(help.contains("/status"));
|
||||||
assert!(help.contains("/model [model]"));
|
assert!(help.contains("/model [model]"));
|
||||||
@@ -4383,7 +4177,6 @@ mod tests {
|
|||||||
assert!(help.contains("/agents"));
|
assert!(help.contains("/agents"));
|
||||||
assert!(help.contains("/skills"));
|
assert!(help.contains("/skills"));
|
||||||
assert!(help.contains("/exit"));
|
assert!(help.contains("/exit"));
|
||||||
assert!(help.contains("Tab cycles slash command matches"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4406,8 +4199,8 @@ mod tests {
|
|||||||
let report = format_resume_report("session.json", 14, 6);
|
let report = format_resume_report("session.json", 14, 6);
|
||||||
assert!(report.contains("Session resumed"));
|
assert!(report.contains("Session resumed"));
|
||||||
assert!(report.contains("Session file session.json"));
|
assert!(report.contains("Session file session.json"));
|
||||||
assert!(report.contains("History 14 messages · 6 turns"));
|
assert!(report.contains("Messages 14"));
|
||||||
assert!(report.contains("/status · /diff · /export"));
|
assert!(report.contains("Turns 6"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4416,7 +4209,6 @@ mod tests {
|
|||||||
assert!(compacted.contains("Compact"));
|
assert!(compacted.contains("Compact"));
|
||||||
assert!(compacted.contains("Result compacted"));
|
assert!(compacted.contains("Result compacted"));
|
||||||
assert!(compacted.contains("Messages removed 8"));
|
assert!(compacted.contains("Messages removed 8"));
|
||||||
assert!(compacted.contains("Use /status"));
|
|
||||||
let skipped = format_compact_report(0, 3, true);
|
let skipped = format_compact_report(0, 3, true);
|
||||||
assert!(skipped.contains("Result skipped"));
|
assert!(skipped.contains("Result skipped"));
|
||||||
}
|
}
|
||||||
@@ -4435,7 +4227,6 @@ mod tests {
|
|||||||
assert!(report.contains("Cache create 3"));
|
assert!(report.contains("Cache create 3"));
|
||||||
assert!(report.contains("Cache read 1"));
|
assert!(report.contains("Cache read 1"));
|
||||||
assert!(report.contains("Total tokens 32"));
|
assert!(report.contains("Total tokens 32"));
|
||||||
assert!(report.contains("/compact"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4443,7 +4234,6 @@ mod tests {
|
|||||||
let report = format_permissions_report("workspace-write");
|
let report = format_permissions_report("workspace-write");
|
||||||
assert!(report.contains("Permissions"));
|
assert!(report.contains("Permissions"));
|
||||||
assert!(report.contains("Active mode workspace-write"));
|
assert!(report.contains("Active mode workspace-write"));
|
||||||
assert!(report.contains("Effect Editing tools can modify files in the workspace"));
|
|
||||||
assert!(report.contains("Modes"));
|
assert!(report.contains("Modes"));
|
||||||
assert!(report.contains("read-only ○ available Read/search tools only"));
|
assert!(report.contains("read-only ○ available Read/search tools only"));
|
||||||
assert!(report.contains("workspace-write ● current Edit files inside the workspace"));
|
assert!(report.contains("workspace-write ● current Edit files inside the workspace"));
|
||||||
@@ -4454,9 +4244,10 @@ mod tests {
|
|||||||
fn permissions_switch_report_is_structured() {
|
fn permissions_switch_report_is_structured() {
|
||||||
let report = format_permissions_switch_report("read-only", "workspace-write");
|
let report = format_permissions_switch_report("read-only", "workspace-write");
|
||||||
assert!(report.contains("Permissions updated"));
|
assert!(report.contains("Permissions updated"));
|
||||||
|
assert!(report.contains("Result mode switched"));
|
||||||
assert!(report.contains("Previous mode read-only"));
|
assert!(report.contains("Previous mode read-only"));
|
||||||
assert!(report.contains("Active mode workspace-write"));
|
assert!(report.contains("Active mode workspace-write"));
|
||||||
assert!(report.contains("Applies to Subsequent tool calls in this REPL"));
|
assert!(report.contains("Applies to subsequent tool calls"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4474,10 +4265,9 @@ mod tests {
|
|||||||
fn model_report_uses_sectioned_layout() {
|
fn model_report_uses_sectioned_layout() {
|
||||||
let report = format_model_report("sonnet", 12, 4);
|
let report = format_model_report("sonnet", 12, 4);
|
||||||
assert!(report.contains("Model"));
|
assert!(report.contains("Model"));
|
||||||
assert!(report.contains("Current sonnet"));
|
assert!(report.contains("Current model sonnet"));
|
||||||
assert!(report.contains("Session 12 messages · 4 turns"));
|
assert!(report.contains("Session messages 12"));
|
||||||
assert!(report.contains("Aliases"));
|
assert!(report.contains("Switch models with /model <name>"));
|
||||||
assert!(report.contains("/model <name> Switch models for this REPL session"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4486,7 +4276,7 @@ mod tests {
|
|||||||
assert!(report.contains("Model updated"));
|
assert!(report.contains("Model updated"));
|
||||||
assert!(report.contains("Previous sonnet"));
|
assert!(report.contains("Previous sonnet"));
|
||||||
assert!(report.contains("Current opus"));
|
assert!(report.contains("Current opus"));
|
||||||
assert!(report.contains("Preserved 9 messages"));
|
assert!(report.contains("Preserved msgs 9"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4521,18 +4311,18 @@ mod tests {
|
|||||||
git_branch: Some("main".to_string()),
|
git_branch: Some("main".to_string()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert!(status.contains("Session"));
|
assert!(status.contains("Status"));
|
||||||
assert!(status.contains("Model sonnet"));
|
assert!(status.contains("Model sonnet"));
|
||||||
assert!(status.contains("Permissions workspace-write"));
|
assert!(status.contains("Permission mode workspace-write"));
|
||||||
assert!(status.contains("Activity 7 messages · 3 turns"));
|
assert!(status.contains("Messages 7"));
|
||||||
assert!(status.contains("Tokens est 128 · latest 10 · total 31"));
|
assert!(status.contains("Latest total 10"));
|
||||||
assert!(status.contains("Folder /tmp/project"));
|
assert!(status.contains("Cumulative total 31"));
|
||||||
|
assert!(status.contains("Cwd /tmp/project"));
|
||||||
assert!(status.contains("Project root /tmp"));
|
assert!(status.contains("Project root /tmp"));
|
||||||
assert!(status.contains("Git branch main"));
|
assert!(status.contains("Git branch main"));
|
||||||
assert!(status.contains("Session file session.json"));
|
assert!(status.contains("Session session.json"));
|
||||||
assert!(status.contains("Config files loaded 2/3"));
|
assert!(status.contains("Config files loaded 2/3"));
|
||||||
assert!(status.contains("Memory files 4"));
|
assert!(status.contains("Memory files 4"));
|
||||||
assert!(status.contains("/session list"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4668,8 +4458,8 @@ mod tests {
|
|||||||
fn repl_help_mentions_history_completion_and_multiline() {
|
fn repl_help_mentions_history_completion_and_multiline() {
|
||||||
let help = render_repl_help();
|
let help = render_repl_help();
|
||||||
assert!(help.contains("Up/Down"));
|
assert!(help.contains("Up/Down"));
|
||||||
assert!(help.contains("Tab cycles"));
|
assert!(help.contains("Tab"));
|
||||||
assert!(help.contains("Shift+Enter or Ctrl+J"));
|
assert!(help.contains("Shift+Enter/Ctrl+J"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -39,27 +39,6 @@ impl CommandRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum SlashCommandCategory {
|
|
||||||
Core,
|
|
||||||
Workspace,
|
|
||||||
Session,
|
|
||||||
Git,
|
|
||||||
Automation,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SlashCommandCategory {
|
|
||||||
const fn title(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Core => "Core flow",
|
|
||||||
Self::Workspace => "Workspace & memory",
|
|
||||||
Self::Session => "Sessions & output",
|
|
||||||
Self::Git => "Git & GitHub",
|
|
||||||
Self::Automation => "Automation & discovery",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct SlashCommandSpec {
|
pub struct SlashCommandSpec {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
@@ -67,7 +46,6 @@ pub struct SlashCommandSpec {
|
|||||||
pub summary: &'static str,
|
pub summary: &'static str,
|
||||||
pub argument_hint: Option<&'static str>,
|
pub argument_hint: Option<&'static str>,
|
||||||
pub resume_supported: bool,
|
pub resume_supported: bool,
|
||||||
pub category: SlashCommandCategory,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||||
@@ -77,7 +55,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Show available slash commands",
|
summary: "Show available slash commands",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Core,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "status",
|
name: "status",
|
||||||
@@ -85,7 +62,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Show current session status",
|
summary: "Show current session status",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Core,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "compact",
|
name: "compact",
|
||||||
@@ -93,7 +69,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Compact local session history",
|
summary: "Compact local session history",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Core,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "model",
|
name: "model",
|
||||||
@@ -101,7 +76,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Show or switch the active model",
|
summary: "Show or switch the active model",
|
||||||
argument_hint: Some("[model]"),
|
argument_hint: Some("[model]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Core,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "permissions",
|
name: "permissions",
|
||||||
@@ -109,7 +83,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Show or switch the active permission mode",
|
summary: "Show or switch the active permission mode",
|
||||||
argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
|
argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Core,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "clear",
|
name: "clear",
|
||||||
@@ -117,7 +90,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Start a fresh local session",
|
summary: "Start a fresh local session",
|
||||||
argument_hint: Some("[--confirm]"),
|
argument_hint: Some("[--confirm]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Session,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "cost",
|
name: "cost",
|
||||||
@@ -125,7 +97,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Show cumulative token usage for this session",
|
summary: "Show cumulative token usage for this session",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Core,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "resume",
|
name: "resume",
|
||||||
@@ -133,7 +104,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Load a saved session into the REPL",
|
summary: "Load a saved session into the REPL",
|
||||||
argument_hint: Some("<session-path>"),
|
argument_hint: Some("<session-path>"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Session,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "config",
|
name: "config",
|
||||||
@@ -141,7 +111,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Inspect Claw config files or merged sections",
|
summary: "Inspect Claw config files or merged sections",
|
||||||
argument_hint: Some("[env|hooks|model|plugins]"),
|
argument_hint: Some("[env|hooks|model|plugins]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Workspace,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "memory",
|
name: "memory",
|
||||||
@@ -149,7 +118,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Inspect loaded Claw instruction memory files",
|
summary: "Inspect loaded Claw instruction memory files",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Workspace,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "init",
|
name: "init",
|
||||||
@@ -157,7 +125,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Create a starter CLAW.md for this repo",
|
summary: "Create a starter CLAW.md for this repo",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Workspace,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "diff",
|
name: "diff",
|
||||||
@@ -165,7 +132,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Show git diff for current workspace changes",
|
summary: "Show git diff for current workspace changes",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Workspace,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "version",
|
name: "version",
|
||||||
@@ -173,7 +139,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Show CLI version and build information",
|
summary: "Show CLI version and build information",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Workspace,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "bughunter",
|
name: "bughunter",
|
||||||
@@ -181,7 +146,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Inspect the codebase for likely bugs",
|
summary: "Inspect the codebase for likely bugs",
|
||||||
argument_hint: Some("[scope]"),
|
argument_hint: Some("[scope]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Automation,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "branch",
|
name: "branch",
|
||||||
@@ -189,7 +153,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "List, create, or switch git branches",
|
summary: "List, create, or switch git branches",
|
||||||
argument_hint: Some("[list|create <name>|switch <name>]"),
|
argument_hint: Some("[list|create <name>|switch <name>]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Git,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "worktree",
|
name: "worktree",
|
||||||
@@ -197,7 +160,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "List, add, remove, or prune git worktrees",
|
summary: "List, add, remove, or prune git worktrees",
|
||||||
argument_hint: Some("[list|add <path> [branch]|remove <path>|prune]"),
|
argument_hint: Some("[list|add <path> [branch]|remove <path>|prune]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Git,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "commit",
|
name: "commit",
|
||||||
@@ -205,7 +167,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Generate a commit message and create a git commit",
|
summary: "Generate a commit message and create a git commit",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Git,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "commit-push-pr",
|
name: "commit-push-pr",
|
||||||
@@ -213,7 +174,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Commit workspace changes, push the branch, and open a PR",
|
summary: "Commit workspace changes, push the branch, and open a PR",
|
||||||
argument_hint: Some("[context]"),
|
argument_hint: Some("[context]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Git,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "pr",
|
name: "pr",
|
||||||
@@ -221,7 +181,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Draft or create a pull request from the conversation",
|
summary: "Draft or create a pull request from the conversation",
|
||||||
argument_hint: Some("[context]"),
|
argument_hint: Some("[context]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Git,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "issue",
|
name: "issue",
|
||||||
@@ -229,7 +188,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Draft or create a GitHub issue from the conversation",
|
summary: "Draft or create a GitHub issue from the conversation",
|
||||||
argument_hint: Some("[context]"),
|
argument_hint: Some("[context]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Git,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "ultraplan",
|
name: "ultraplan",
|
||||||
@@ -237,7 +195,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Run a deep planning prompt with multi-step reasoning",
|
summary: "Run a deep planning prompt with multi-step reasoning",
|
||||||
argument_hint: Some("[task]"),
|
argument_hint: Some("[task]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Automation,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "teleport",
|
name: "teleport",
|
||||||
@@ -245,7 +202,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Jump to a file or symbol by searching the workspace",
|
summary: "Jump to a file or symbol by searching the workspace",
|
||||||
argument_hint: Some("<symbol-or-path>"),
|
argument_hint: Some("<symbol-or-path>"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Workspace,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "debug-tool-call",
|
name: "debug-tool-call",
|
||||||
@@ -253,7 +209,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Replay the last tool call with debug details",
|
summary: "Replay the last tool call with debug details",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Automation,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "export",
|
name: "export",
|
||||||
@@ -261,7 +216,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "Export the current conversation to a file",
|
summary: "Export the current conversation to a file",
|
||||||
argument_hint: Some("[file]"),
|
argument_hint: Some("[file]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Session,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "session",
|
name: "session",
|
||||||
@@ -269,7 +223,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "List or switch managed local sessions",
|
summary: "List or switch managed local sessions",
|
||||||
argument_hint: Some("[list|switch <session-id>]"),
|
argument_hint: Some("[list|switch <session-id>]"),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Session,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "plugin",
|
name: "plugin",
|
||||||
@@ -279,7 +232,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
"[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
|
"[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
|
||||||
),
|
),
|
||||||
resume_supported: false,
|
resume_supported: false,
|
||||||
category: SlashCommandCategory::Automation,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "agents",
|
name: "agents",
|
||||||
@@ -287,7 +239,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "List configured agents",
|
summary: "List configured agents",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Automation,
|
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "skills",
|
name: "skills",
|
||||||
@@ -295,7 +246,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
summary: "List available skills",
|
summary: "List available skills",
|
||||||
argument_hint: None,
|
argument_hint: None,
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Automation,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -487,131 +437,38 @@ pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
|
|||||||
pub fn render_slash_command_help() -> String {
|
pub fn render_slash_command_help() -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
"Slash commands".to_string(),
|
"Slash commands".to_string(),
|
||||||
" Tab completes commands inside the REPL.".to_string(),
|
" [resume] means the command also works with --resume SESSION.json".to_string(),
|
||||||
" [resume] works with --resume SESSION.json.".to_string(),
|
|
||||||
];
|
];
|
||||||
|
for spec in slash_command_specs() {
|
||||||
for category in [
|
let name = match spec.argument_hint {
|
||||||
SlashCommandCategory::Core,
|
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
||||||
SlashCommandCategory::Workspace,
|
None => format!("/{}", spec.name),
|
||||||
SlashCommandCategory::Session,
|
};
|
||||||
SlashCommandCategory::Git,
|
let alias_suffix = if spec.aliases.is_empty() {
|
||||||
SlashCommandCategory::Automation,
|
String::new()
|
||||||
] {
|
} else {
|
||||||
lines.push(String::new());
|
format!(
|
||||||
lines.push(category.title().to_string());
|
" (aliases: {})",
|
||||||
lines.extend(
|
spec.aliases
|
||||||
slash_command_specs()
|
.iter()
|
||||||
.iter()
|
.map(|alias| format!("/{alias}"))
|
||||||
.filter(|spec| spec.category == category)
|
.collect::<Vec<_>>()
|
||||||
.map(render_slash_command_entry),
|
.join(", ")
|
||||||
);
|
)
|
||||||
|
};
|
||||||
|
let resume = if spec.resume_supported {
|
||||||
|
" [resume]"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
lines.push(format!(
|
||||||
|
" {name:<20} {}{alias_suffix}{resume}",
|
||||||
|
spec.summary
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_slash_command_entry(spec: &SlashCommandSpec) -> String {
|
|
||||||
let alias_suffix = if spec.aliases.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
" (aliases: {})",
|
|
||||||
spec.aliases
|
|
||||||
.iter()
|
|
||||||
.map(|alias| format!("/{alias}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let resume = if spec.resume_supported {
|
|
||||||
" [resume]"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
format!(
|
|
||||||
" {name:<46} {}{alias_suffix}{resume}",
|
|
||||||
spec.summary,
|
|
||||||
name = render_slash_command_name(spec),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_slash_command_name(spec: &SlashCommandSpec) -> String {
|
|
||||||
match spec.argument_hint {
|
|
||||||
Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
|
|
||||||
None => format!("/{}", spec.name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn levenshtein_distance(left: &str, right: &str) -> usize {
|
|
||||||
if left == right {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if left.is_empty() {
|
|
||||||
return right.chars().count();
|
|
||||||
}
|
|
||||||
if right.is_empty() {
|
|
||||||
return left.chars().count();
|
|
||||||
}
|
|
||||||
|
|
||||||
let right_chars = right.chars().collect::<Vec<_>>();
|
|
||||||
let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
|
|
||||||
let mut current = vec![0; right_chars.len() + 1];
|
|
||||||
|
|
||||||
for (left_index, left_char) in left.chars().enumerate() {
|
|
||||||
current[0] = left_index + 1;
|
|
||||||
for (right_index, right_char) in right_chars.iter().enumerate() {
|
|
||||||
let cost = usize::from(left_char != *right_char);
|
|
||||||
current[right_index + 1] = (previous[right_index + 1] + 1)
|
|
||||||
.min(current[right_index] + 1)
|
|
||||||
.min(previous[right_index] + cost);
|
|
||||||
}
|
|
||||||
std::mem::swap(&mut previous, &mut current);
|
|
||||||
}
|
|
||||||
|
|
||||||
previous[right_chars.len()]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn suggest_slash_commands(input: &str, limit: usize) -> Vec<String> {
|
|
||||||
let normalized = input.trim().trim_start_matches('/').to_ascii_lowercase();
|
|
||||||
if normalized.is_empty() || limit == 0 {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ranked = slash_command_specs()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|spec| {
|
|
||||||
let score = std::iter::once(spec.name)
|
|
||||||
.chain(spec.aliases.iter().copied())
|
|
||||||
.map(str::to_ascii_lowercase)
|
|
||||||
.filter_map(|alias| {
|
|
||||||
if alias == normalized {
|
|
||||||
Some((0_usize, alias.len()))
|
|
||||||
} else if alias.starts_with(&normalized) {
|
|
||||||
Some((1, alias.len()))
|
|
||||||
} else if alias.contains(&normalized) {
|
|
||||||
Some((2, alias.len()))
|
|
||||||
} else {
|
|
||||||
let distance = levenshtein_distance(&alias, &normalized);
|
|
||||||
(distance <= 2).then_some((3 + distance, alias.len()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.min();
|
|
||||||
|
|
||||||
score.map(|(bucket, len)| (bucket, len, render_slash_command_name(spec)))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
ranked.sort_by(|left, right| left.cmp(right));
|
|
||||||
ranked.dedup_by(|left, right| left.2 == right.2);
|
|
||||||
ranked
|
|
||||||
.into_iter()
|
|
||||||
.take(limit)
|
|
||||||
.map(|(_, _, display)| display)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SlashCommandResult {
|
pub struct SlashCommandResult {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
@@ -1795,8 +1652,7 @@ mod tests {
|
|||||||
handle_worktree_slash_command, load_agents_from_roots, load_skills_from_roots,
|
handle_worktree_slash_command, load_agents_from_roots, load_skills_from_roots,
|
||||||
render_agents_report, render_plugins_report, render_skills_report,
|
render_agents_report, render_plugins_report, render_skills_report,
|
||||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
||||||
suggest_slash_commands, CommitPushPrRequest, DefinitionSource, SkillOrigin, SkillRoot,
|
CommitPushPrRequest, DefinitionSource, SkillOrigin, SkillRoot, SlashCommand,
|
||||||
SlashCommand,
|
|
||||||
};
|
};
|
||||||
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
||||||
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
||||||
@@ -2109,11 +1965,6 @@ mod tests {
|
|||||||
fn renders_help_from_shared_specs() {
|
fn renders_help_from_shared_specs() {
|
||||||
let help = render_slash_command_help();
|
let help = render_slash_command_help();
|
||||||
assert!(help.contains("works with --resume SESSION.json"));
|
assert!(help.contains("works with --resume SESSION.json"));
|
||||||
assert!(help.contains("Core flow"));
|
|
||||||
assert!(help.contains("Workspace & memory"));
|
|
||||||
assert!(help.contains("Sessions & output"));
|
|
||||||
assert!(help.contains("Git & GitHub"));
|
|
||||||
assert!(help.contains("Automation & discovery"));
|
|
||||||
assert!(help.contains("/help"));
|
assert!(help.contains("/help"));
|
||||||
assert!(help.contains("/status"));
|
assert!(help.contains("/status"));
|
||||||
assert!(help.contains("/compact"));
|
assert!(help.contains("/compact"));
|
||||||
@@ -2149,13 +2000,6 @@ mod tests {
|
|||||||
assert_eq!(resume_supported_slash_commands().len(), 13);
|
assert_eq!(resume_supported_slash_commands().len(), 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn suggests_close_slash_commands() {
|
|
||||||
let suggestions = suggest_slash_commands("stats", 3);
|
|
||||||
assert!(!suggestions.is_empty());
|
|
||||||
assert_eq!(suggestions[0], "/status");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn compacts_sessions_via_slash_command() {
|
fn compacts_sessions_via_slash_command() {
|
||||||
let session = Session {
|
let session = Session {
|
||||||
|
|||||||
Reference in New Issue
Block a user