minor README tweaks + rust progress
This commit is contained in:
parent
c99507ca1e
commit
45f7ac9071
9 changed files with 628 additions and 25 deletions
37
README.md
37
README.md
|
|
@ -7,7 +7,7 @@ The process was explicitly two-phase:
|
||||||
|
|
||||||
Specification [`spec/`](https://github.com/kuberwastaken/claude-code/tree/main/spec) - An AI agent analyzed the source and produced exhaustive behavioral specifications and improvements, deviated from the original: architecture, data flows, tool contracts, system designs. No source code was carried forward.
|
Specification [`spec/`](https://github.com/kuberwastaken/claude-code/tree/main/spec) - An AI agent analyzed the source and produced exhaustive behavioral specifications and improvements, deviated from the original: architecture, data flows, tool contracts, system designs. No source code was carried forward.
|
||||||
|
|
||||||
Implementation [`src/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust)- A separate AI agent implemented from the spec alone, never referencing the original TypeScript. The output is idiomatic Rust that reproduces the behavior, not the expression.
|
Implementation [`src-rust/`](https://github.com/kuberwastaken/claude-code/tree/main/src-rust)- A separate AI agent implemented from the spec alone, never referencing the original TypeScript. The output is idiomatic Rust that reproduces the behavior, not the expression.
|
||||||
|
|
||||||
This mirrors the legal precedent established by Phoenix Technologies v. IBM (1984) — clean-room engineering of the BIOS — and the principle from Baker v. Selden (1879) that copyright protects expression, not ideas or behavior.
|
This mirrors the legal precedent established by Phoenix Technologies v. IBM (1984) — clean-room engineering of the BIOS — and the principle from Baker v. Selden (1879) that copyright protects expression, not ideas or behavior.
|
||||||
|
|
||||||
|
|
@ -353,6 +353,14 @@ Claude Code's tool system lives in [`tools/`](https://github.com/kuberwastaken/c
|
||||||
| **WorkflowTool** | Execute workflow scripts |
|
| **WorkflowTool** | Execute workflow scripts |
|
||||||
| **ConfigTool** | Modify settings (**internal only**) |
|
| **ConfigTool** | Modify settings (**internal only**) |
|
||||||
| **TungstenTool** | Advanced features (**internal only**) |
|
| **TungstenTool** | Advanced features (**internal only**) |
|
||||||
|
| **MCPTool** | Generic MCP tool execution |
|
||||||
|
| **McpAuthTool** | MCP server authentication |
|
||||||
|
| **SyntheticOutputTool** | Structured output via dynamic JSON schemas |
|
||||||
|
| **SuggestBackgroundPRTool** | Suggest background PRs (**internal only**) |
|
||||||
|
| **VerifyPlanExecutionTool** | Verify plan execution (gated by `CLAUDE_CODE_VERIFY_PLAN`) |
|
||||||
|
| **CtxInspectTool** | Context window inspection (gated by `CONTEXT_COLLAPSE`) |
|
||||||
|
| **TerminalCaptureTool** | Terminal panel capture (gated by `TERMINAL_PANEL`) |
|
||||||
|
| **CronCreateTool** / **CronDeleteTool** / **CronListTool** | Granular cron job management (under `ScheduleCronTool/`) |
|
||||||
| **SendUserFile** / **PushNotification** / **SubscribePR** | KAIROS-exclusive tools |
|
| **SendUserFile** / **PushNotification** / **SubscribePR** | KAIROS-exclusive tools |
|
||||||
|
|
||||||
Tools are registered via `getAllBaseTools()` and filtered by feature gates, user type, environment flags, and permission deny rules. There's a **tool schema cache** ([`toolSchemaCache.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/tools/src/lib.rs)) that caches JSON schemas for prompt efficiency.
|
Tools are registered via `getAllBaseTools()` and filtered by feature gates, user type, environment flags, and permission deny rules. There's a **tool schema cache** ([`toolSchemaCache.ts`](https://github.com/kuberwastaken/claude-code/blob/main/src-rust/crates/tools/src/lib.rs)) that caches JSON schemas for prompt efficiency.
|
||||||
|
|
@ -401,6 +409,33 @@ The [`constants/betas.ts`](https://github.com/kuberwastaken/claude-code/blob/mai
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Upcoming Models - Capybara, Opus 4.7, and Sonnet 4.8
|
||||||
|
|
||||||
|
The codebase contains references to unreleased Anthropic models that haven't been publicly announced:
|
||||||
|
|
||||||
|
- **Claude "Capybara"** - A new model family already in version 2, with a variant called `capybara-v2-fast` being prepared with a **1M context window**
|
||||||
|
- **Capybara comes in "fast" and regular thinking** tiers
|
||||||
|
- **Opus 4.7** and **Sonnet 4.8** are already referenced within the code
|
||||||
|
|
||||||
|
### Production Engineering Around Capybara
|
||||||
|
|
||||||
|
The code reveals that Anthropic observed a **real production failure mode**: Capybara can prematurely stop generating when the prompt shape resembles a turn boundary after tool results. Rather than waiting for a model fix, they mitigated it with **prompt-shape surgery**:
|
||||||
|
|
||||||
|
1. **Force a safe boundary marker** (`Tool loaded.`) to prevent ambiguous turn boundaries
|
||||||
|
2. **Relocate risky sibling blocks** that could trigger premature stops
|
||||||
|
3. **Smoosh reminder text into tool results** to maintain generation flow
|
||||||
|
4. **Add non-empty markers for empty tool outputs** to avoid confusing the model
|
||||||
|
|
||||||
|
All of this is wrapped with **kill-switchable gates** (`tengu_*` prefixed flags) so rollout can be staged and reverted quickly.
|
||||||
|
|
||||||
|
- Comments include **concrete A/B test evidence** (not hand-wavy), which typically means this area was **launch-critical** and closely monitored
|
||||||
|
- Comments like *"un-gate once validated on external via A/B"* confirm that **ant/internal users are canary lanes** before broader rollout
|
||||||
|
- The strongest interpretation: Anthropic is working toward a **Capybara model family** with a fast-tier variant (`capybara-v2-fast`), supporting up to **1M context**
|
||||||
|
|
||||||
|
Nothing confirms a launch date or official SKU naming, but the implementation signatures fit a model family that is actively being prepared for release ;)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Feature Gating - Internal vs. External Builds
|
## Feature Gating - Internal vs. External Builds
|
||||||
|
|
||||||
This is one of the most architecturally interesting parts of the codebase.
|
This is one of the most architecturally interesting parts of the codebase.
|
||||||
|
|
|
||||||
|
|
@ -151,10 +151,6 @@ struct Cli {
|
||||||
#[arg(long = "dangerously-skip-permissions", action = ArgAction::SetTrue)]
|
#[arg(long = "dangerously-skip-permissions", action = ArgAction::SetTrue)]
|
||||||
dangerously_skip_permissions: bool,
|
dangerously_skip_permissions: bool,
|
||||||
|
|
||||||
/// Show version and exit
|
|
||||||
#[arg(long = "version", short = 'V', action = ArgAction::SetTrue)]
|
|
||||||
version_flag: bool,
|
|
||||||
|
|
||||||
/// Dump the system prompt to stdout and exit
|
/// Dump the system prompt to stdout and exit
|
||||||
#[arg(long = "dump-system-prompt", action = ArgAction::SetTrue, hide = true)]
|
#[arg(long = "dump-system-prompt", action = ArgAction::SetTrue, hide = true)]
|
||||||
dump_system_prompt: bool,
|
dump_system_prompt: bool,
|
||||||
|
|
@ -449,6 +445,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
max_turns: cli.max_turns,
|
max_turns: cli.max_turns,
|
||||||
system_prompt: Some(system_prompt),
|
system_prompt: Some(system_prompt),
|
||||||
append_system_prompt: None,
|
append_system_prompt: None,
|
||||||
|
output_style: config.effective_output_style(),
|
||||||
|
working_directory: Some(cwd.display().to_string()),
|
||||||
thinking_budget: None,
|
thinking_budget: None,
|
||||||
temperature: None,
|
temperature: None,
|
||||||
};
|
};
|
||||||
|
|
@ -805,6 +803,11 @@ async fn run_interactive(
|
||||||
app.status_message =
|
app.status_message =
|
||||||
Some("Configuration updated.".to_string());
|
Some("Configuration updated.".to_string());
|
||||||
}
|
}
|
||||||
|
Some(CommandResult::ConfigChangeMessage(new_cfg, msg)) => {
|
||||||
|
cmd_ctx.config = new_cfg.clone();
|
||||||
|
app.config = new_cfg;
|
||||||
|
app.status_message = Some(msg);
|
||||||
|
}
|
||||||
Some(CommandResult::UserMessage(msg)) => {
|
Some(CommandResult::UserMessage(msg)) => {
|
||||||
// Inject as user turn
|
// Inject as user turn
|
||||||
messages.push(cc_core::types::Message::user(msg.clone()));
|
messages.push(cc_core::types::Message::user(msg.clone()));
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@
|
||||||
// Each command is a struct implementing the `SlashCommand` trait.
|
// Each command is a struct implementing the `SlashCommand` trait.
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cc_core::config::Config;
|
use cc_core::config::{Config, Settings, Theme};
|
||||||
use cc_core::cost::CostTracker;
|
use cc_core::cost::CostTracker;
|
||||||
use cc_core::types::Message;
|
use cc_core::types::Message;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -34,6 +35,8 @@ pub enum CommandResult {
|
||||||
UserMessage(String),
|
UserMessage(String),
|
||||||
/// Modify the configuration.
|
/// Modify the configuration.
|
||||||
ConfigChange(Config),
|
ConfigChange(Config),
|
||||||
|
/// Modify the configuration and show a specific status message.
|
||||||
|
ConfigChangeMessage(Config, String),
|
||||||
/// Clear the conversation.
|
/// Clear the conversation.
|
||||||
ClearConversation,
|
ClearConversation,
|
||||||
/// Replace the conversation with a specific message list (used by /rewind).
|
/// Replace the conversation with a specific message list (used by /rewind).
|
||||||
|
|
@ -111,6 +114,141 @@ pub struct RenameCommand;
|
||||||
pub struct EffortCommand;
|
pub struct EffortCommand;
|
||||||
pub struct SummaryCommand;
|
pub struct SummaryCommand;
|
||||||
pub struct CommitCommand;
|
pub struct CommitCommand;
|
||||||
|
pub struct ThemeCommand;
|
||||||
|
pub struct OutputStyleCommand;
|
||||||
|
pub struct KeybindingsCommand;
|
||||||
|
pub struct PrivacySettingsCommand;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct KeybindingTemplateFile {
|
||||||
|
#[serde(rename = "$schema")]
|
||||||
|
schema: &'static str,
|
||||||
|
#[serde(rename = "$docs")]
|
||||||
|
docs: &'static str,
|
||||||
|
bindings: Vec<KeybindingTemplateBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct KeybindingTemplateBlock {
|
||||||
|
context: String,
|
||||||
|
bindings: BTreeMap<String, Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_settings_mutation<F>(mutate: F) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Settings),
|
||||||
|
{
|
||||||
|
let mut settings = Settings::load_sync()?;
|
||||||
|
mutate(&mut settings);
|
||||||
|
settings.save_sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_with_system(target: &str) -> std::io::Result<()> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let ps_cmd = format!("Start-Process '{}'", target.replace('\'', "''"));
|
||||||
|
std::process::Command::new("powershell")
|
||||||
|
.args(["-NoProfile", "-NonInteractive", "-Command", &ps_cmd])
|
||||||
|
.stdin(std::process::Stdio::null())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
std::process::Command::new("open")
|
||||||
|
.arg(target)
|
||||||
|
.stdin(std::process::Stdio::null())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
{
|
||||||
|
std::process::Command::new("xdg-open")
|
||||||
|
.arg(target)
|
||||||
|
.stdin(std::process::Stdio::null())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_keystroke(keystroke: &cc_core::keybindings::ParsedKeystroke) -> String {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if keystroke.ctrl {
|
||||||
|
parts.push("ctrl".to_string());
|
||||||
|
}
|
||||||
|
if keystroke.alt {
|
||||||
|
parts.push("alt".to_string());
|
||||||
|
}
|
||||||
|
if keystroke.shift {
|
||||||
|
parts.push("shift".to_string());
|
||||||
|
}
|
||||||
|
if keystroke.meta {
|
||||||
|
parts.push("meta".to_string());
|
||||||
|
}
|
||||||
|
parts.push(match keystroke.key.as_str() {
|
||||||
|
"space" => "space".to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
});
|
||||||
|
parts.join("+")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_chord(chord: &[cc_core::keybindings::ParsedKeystroke]) -> String {
|
||||||
|
chord
|
||||||
|
.iter()
|
||||||
|
.map(format_keystroke)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_keybindings_template() -> anyhow::Result<String> {
|
||||||
|
let mut grouped: BTreeMap<String, BTreeMap<String, Option<String>>> = BTreeMap::new();
|
||||||
|
for binding in cc_core::keybindings::default_bindings() {
|
||||||
|
let chord = format_chord(&binding.chord);
|
||||||
|
if cc_core::keybindings::NON_REBINDABLE.contains(&chord.as_str()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
grouped
|
||||||
|
.entry(format!("{:?}", binding.context))
|
||||||
|
.or_default()
|
||||||
|
.insert(chord, binding.action.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = KeybindingTemplateFile {
|
||||||
|
schema: "https://www.schemastore.org/claude-code-keybindings.json",
|
||||||
|
docs: "https://code.claude.com/docs/en/keybindings",
|
||||||
|
bindings: grouped
|
||||||
|
.into_iter()
|
||||||
|
.map(|(context, bindings)| KeybindingTemplateBlock { context, bindings })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"{}\n",
|
||||||
|
serde_json::to_string_pretty(&template)?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_theme(name: &str) -> Option<Theme> {
|
||||||
|
match name.trim().to_lowercase().as_str() {
|
||||||
|
"default" | "system" => Some(Theme::Default),
|
||||||
|
"dark" => Some(Theme::Dark),
|
||||||
|
"light" => Some(Theme::Light),
|
||||||
|
custom if !custom.is_empty() => Some(Theme::Custom(custom.to_string())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_output_style_name(config: &Config) -> &str {
|
||||||
|
config.output_style.as_deref().unwrap_or("default")
|
||||||
|
}
|
||||||
|
|
||||||
// ---- /help ---------------------------------------------------------------
|
// ---- /help ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -262,9 +400,311 @@ impl SlashCommand for ConfigCommand {
|
||||||
fn name(&self) -> &str { "config" }
|
fn name(&self) -> &str { "config" }
|
||||||
fn description(&self) -> &str { "Show or modify configuration settings" }
|
fn description(&self) -> &str { "Show or modify configuration settings" }
|
||||||
|
|
||||||
|
async fn execute(&self, args: &str, ctx: &mut CommandContext) -> CommandResult {
|
||||||
|
let args = args.trim();
|
||||||
|
if args.is_empty() || matches!(args, "show" | "get") {
|
||||||
|
let json = serde_json::to_string_pretty(&ctx.config).unwrap_or_default();
|
||||||
|
return CommandResult::Message(format!(
|
||||||
|
"Current configuration:\n{}\n\nUsage:\n /config\n /config set theme <default|dark|light>\n /config set output-style <default|concise|explanatory|learning|formal|casual>\n /config set model <model>\n /config set permission-mode <default|accept-edits|bypass-permissions|plan>\n /config unset <model|output-style>",
|
||||||
|
json
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = args.strip_prefix("get ").map(str::trim) {
|
||||||
|
return match key {
|
||||||
|
"theme" => CommandResult::Message(format!("theme = {:?}", ctx.config.theme)),
|
||||||
|
"output-style" | "output_style" => CommandResult::Message(format!(
|
||||||
|
"output-style = {}",
|
||||||
|
current_output_style_name(&ctx.config)
|
||||||
|
)),
|
||||||
|
"model" => CommandResult::Message(format!(
|
||||||
|
"model = {}",
|
||||||
|
ctx.config.effective_model()
|
||||||
|
)),
|
||||||
|
"permission-mode" | "permission_mode" => CommandResult::Message(format!(
|
||||||
|
"permission-mode = {:?}",
|
||||||
|
ctx.config.permission_mode
|
||||||
|
)),
|
||||||
|
other => CommandResult::Error(format!("Unknown config key '{}'", other)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = args.strip_prefix("unset ").map(str::trim) {
|
||||||
|
return match key {
|
||||||
|
"model" => {
|
||||||
|
let mut new_config = ctx.config.clone();
|
||||||
|
new_config.model = None;
|
||||||
|
if let Err(err) = save_settings_mutation(|settings| settings.config.model = None)
|
||||||
|
{
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Failed to save configuration: {}",
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
CommandResult::ConfigChangeMessage(
|
||||||
|
new_config,
|
||||||
|
"Model reset to the default for new sessions.".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"output-style" | "output_style" => {
|
||||||
|
let mut new_config = ctx.config.clone();
|
||||||
|
new_config.output_style = None;
|
||||||
|
if let Err(err) =
|
||||||
|
save_settings_mutation(|settings| settings.config.output_style = None)
|
||||||
|
{
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Failed to save configuration: {}",
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
CommandResult::ConfigChangeMessage(
|
||||||
|
new_config,
|
||||||
|
"Output style reset to default.".to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
other => CommandResult::Error(format!("Unknown config key '{}'", other)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parts = args.splitn(3, ' ');
|
||||||
|
let command = parts.next().unwrap_or_default();
|
||||||
|
let key = parts.next().unwrap_or_default().trim();
|
||||||
|
let value = parts.next().unwrap_or_default().trim();
|
||||||
|
if command != "set" || key.is_empty() || value.is_empty() {
|
||||||
|
return CommandResult::Error("Usage: /config set <key> <value>".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"theme" => {
|
||||||
|
let Some(theme) = parse_theme(value) else {
|
||||||
|
return CommandResult::Error(
|
||||||
|
"Theme must be one of: default, dark, light".to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let mut new_config = ctx.config.clone();
|
||||||
|
new_config.theme = theme.clone();
|
||||||
|
if let Err(err) =
|
||||||
|
save_settings_mutation(|settings| settings.config.theme = theme.clone())
|
||||||
|
{
|
||||||
|
return CommandResult::Error(format!("Failed to save configuration: {}", err));
|
||||||
|
}
|
||||||
|
CommandResult::ConfigChangeMessage(
|
||||||
|
new_config,
|
||||||
|
format!("Theme set to {}.", value.trim().to_lowercase()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"output-style" | "output_style" => {
|
||||||
|
let normalized = value.trim().to_lowercase();
|
||||||
|
let valid = ["default", "concise", "explanatory", "learning", "formal", "casual"];
|
||||||
|
if !valid.contains(&normalized.as_str()) {
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Unsupported output style '{}'. Use one of: {}",
|
||||||
|
value,
|
||||||
|
valid.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_config = ctx.config.clone();
|
||||||
|
new_config.output_style =
|
||||||
|
(normalized != "default").then(|| normalized.clone());
|
||||||
|
if let Err(err) = save_settings_mutation(|settings| {
|
||||||
|
settings.config.output_style =
|
||||||
|
(normalized != "default").then(|| normalized.clone());
|
||||||
|
}) {
|
||||||
|
return CommandResult::Error(format!("Failed to save configuration: {}", err));
|
||||||
|
}
|
||||||
|
CommandResult::ConfigChangeMessage(
|
||||||
|
new_config,
|
||||||
|
format!(
|
||||||
|
"Output style set to {}. Changes take effect on the next request.",
|
||||||
|
normalized
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"model" => {
|
||||||
|
let mut new_config = ctx.config.clone();
|
||||||
|
new_config.model = Some(value.to_string());
|
||||||
|
if let Err(err) = save_settings_mutation(|settings| {
|
||||||
|
settings.config.model = Some(value.to_string());
|
||||||
|
}) {
|
||||||
|
return CommandResult::Error(format!("Failed to save configuration: {}", err));
|
||||||
|
}
|
||||||
|
CommandResult::ConfigChangeMessage(
|
||||||
|
new_config,
|
||||||
|
format!("Model set to {}.", value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"permission-mode" | "permission_mode" => {
|
||||||
|
let mode = match value.trim().to_lowercase().as_str() {
|
||||||
|
"default" => cc_core::config::PermissionMode::Default,
|
||||||
|
"accept-edits" | "accept_edits" => {
|
||||||
|
cc_core::config::PermissionMode::AcceptEdits
|
||||||
|
}
|
||||||
|
"bypass-permissions" | "bypass_permissions" => {
|
||||||
|
cc_core::config::PermissionMode::BypassPermissions
|
||||||
|
}
|
||||||
|
"plan" => cc_core::config::PermissionMode::Plan,
|
||||||
|
_ => {
|
||||||
|
return CommandResult::Error(
|
||||||
|
"Permission mode must be one of: default, accept-edits, bypass-permissions, plan"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_config = ctx.config.clone();
|
||||||
|
new_config.permission_mode = mode.clone();
|
||||||
|
if let Err(err) = save_settings_mutation(|settings| {
|
||||||
|
settings.config.permission_mode = mode.clone();
|
||||||
|
}) {
|
||||||
|
return CommandResult::Error(format!("Failed to save configuration: {}", err));
|
||||||
|
}
|
||||||
|
CommandResult::ConfigChangeMessage(
|
||||||
|
new_config,
|
||||||
|
format!("Permission mode set to {}.", value.trim().to_lowercase()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
other => CommandResult::Error(format!("Unknown config key '{}'", other)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- /theme --------------------------------------------------------------
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SlashCommand for ThemeCommand {
|
||||||
|
fn name(&self) -> &str { "theme" }
|
||||||
|
fn aliases(&self) -> Vec<&str> { vec!["color"] }
|
||||||
|
fn description(&self) -> &str { "Show or change the current theme" }
|
||||||
|
fn help(&self) -> &str {
|
||||||
|
"Usage: /theme [default|dark|light]\n\
|
||||||
|
Without arguments, shows the active theme. With an argument, updates the theme for this and future sessions."
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, args: &str, ctx: &mut CommandContext) -> CommandResult {
|
||||||
|
let args = args.trim();
|
||||||
|
if args.is_empty() {
|
||||||
|
return CommandResult::Message(format!(
|
||||||
|
"Current theme: {:?}\nUse /theme <default|dark|light> to change it.",
|
||||||
|
ctx.config.theme
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(theme) = parse_theme(args) else {
|
||||||
|
return CommandResult::Error(
|
||||||
|
"Theme must be one of: default, dark, light".to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut new_config = ctx.config.clone();
|
||||||
|
new_config.theme = theme.clone();
|
||||||
|
if let Err(err) = save_settings_mutation(|settings| settings.config.theme = theme.clone())
|
||||||
|
{
|
||||||
|
return CommandResult::Error(format!("Failed to save theme: {}", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandResult::ConfigChangeMessage(
|
||||||
|
new_config,
|
||||||
|
format!("Theme set to {}.", args.to_lowercase()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- /output-style -------------------------------------------------------
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SlashCommand for OutputStyleCommand {
|
||||||
|
fn name(&self) -> &str { "output-style" }
|
||||||
|
fn description(&self) -> &str { "Show the output-style migration guidance" }
|
||||||
|
|
||||||
async fn execute(&self, _args: &str, ctx: &mut CommandContext) -> CommandResult {
|
async fn execute(&self, _args: &str, ctx: &mut CommandContext) -> CommandResult {
|
||||||
let json = serde_json::to_string_pretty(&ctx.config).unwrap_or_default();
|
CommandResult::Message(format!(
|
||||||
CommandResult::Message(format!("Current configuration:\n{}", json))
|
"/output-style has been deprecated. Use /config to change your output style, or set it in your settings file. Changes take effect on the next session.\nCurrent output style: {}",
|
||||||
|
current_output_style_name(&ctx.config)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- /keybindings --------------------------------------------------------
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SlashCommand for KeybindingsCommand {
|
||||||
|
fn name(&self) -> &str { "keybindings" }
|
||||||
|
fn description(&self) -> &str { "Create or open ~/.claude/keybindings.json" }
|
||||||
|
|
||||||
|
async fn execute(&self, _args: &str, _ctx: &mut CommandContext) -> CommandResult {
|
||||||
|
let config_dir = Settings::config_dir();
|
||||||
|
let path = config_dir.join("keybindings.json");
|
||||||
|
let existed = path.exists();
|
||||||
|
|
||||||
|
if !existed {
|
||||||
|
if let Err(err) = std::fs::create_dir_all(&config_dir) {
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Failed to create {}: {}",
|
||||||
|
config_dir.display(),
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = match generate_keybindings_template() {
|
||||||
|
Ok(template) => template,
|
||||||
|
Err(err) => {
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Failed to generate keybindings template: {}",
|
||||||
|
err
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = std::fs::write(&path, template) {
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Failed to write {}: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match open_with_system(&path.display().to_string()) {
|
||||||
|
Ok(_) => CommandResult::Message(if existed {
|
||||||
|
format!("Opened {} in your editor.", path.display())
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Created {} with a template and opened it in your editor.",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Err(err) => CommandResult::Message(if existed {
|
||||||
|
format!(
|
||||||
|
"Opened {}. Could not launch an editor automatically: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Created {} with a template. Could not launch an editor automatically: {}",
|
||||||
|
path.display(),
|
||||||
|
err
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- /privacy-settings ---------------------------------------------------
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SlashCommand for PrivacySettingsCommand {
|
||||||
|
fn name(&self) -> &str { "privacy-settings" }
|
||||||
|
fn description(&self) -> &str { "Open Claude privacy settings" }
|
||||||
|
|
||||||
|
async fn execute(&self, _args: &str, _ctx: &mut CommandContext) -> CommandResult {
|
||||||
|
let url = "https://claude.ai/settings/data-privacy-controls";
|
||||||
|
let fallback = format!("Review and manage your privacy settings at {}", url);
|
||||||
|
match open_with_system(url) {
|
||||||
|
Ok(_) => CommandResult::Message(format!("Opened privacy settings: {}", url)),
|
||||||
|
Err(_) => CommandResult::Message(fallback),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1086,6 +1526,10 @@ pub fn all_commands() -> Vec<Box<dyn SlashCommand>> {
|
||||||
Box::new(TasksCommand),
|
Box::new(TasksCommand),
|
||||||
Box::new(SessionCommand),
|
Box::new(SessionCommand),
|
||||||
Box::new(ThinkingCommand),
|
Box::new(ThinkingCommand),
|
||||||
|
Box::new(ThemeCommand),
|
||||||
|
Box::new(OutputStyleCommand),
|
||||||
|
Box::new(KeybindingsCommand),
|
||||||
|
Box::new(PrivacySettingsCommand),
|
||||||
// New commands
|
// New commands
|
||||||
Box::new(ExportCommand),
|
Box::new(ExportCommand),
|
||||||
Box::new(SkillsCommand),
|
Box::new(SkillsCommand),
|
||||||
|
|
|
||||||
|
|
@ -128,10 +128,28 @@ impl NamedCommand for AddDirCommand {
|
||||||
Err(e) => return CommandResult::Error(format!("Cannot resolve path: {e}")),
|
Err(e) => return CommandResult::Error(format!("Cannot resolve path: {e}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: persist to settings.json `workspacePaths` array
|
let mut settings = match cc_core::config::Settings::load_sync() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Failed to load settings before updating workspace paths: {e}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !settings.config.workspace_paths.iter().any(|p| p == &abs_path) {
|
||||||
|
settings.config.workspace_paths.push(abs_path.clone());
|
||||||
|
if let Err(e) = settings.save_sync() {
|
||||||
|
return CommandResult::Error(format!(
|
||||||
|
"Added {} for this session, but failed to save settings: {}",
|
||||||
|
abs_path.display(),
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CommandResult::Message(format!(
|
CommandResult::Message(format!(
|
||||||
"Added {} to allowed workspace paths.\n\
|
"Added {} to allowed workspace paths.",
|
||||||
Note: restart Claude Code for the change to take effect.",
|
|
||||||
abs_path.display()
|
abs_path.display()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,9 @@ pub fn parse_keystroke(s: &str) -> Option<ParsedKeystroke> {
|
||||||
|
|
||||||
for part in s.split('+') {
|
for part in s.split('+') {
|
||||||
let part = part.trim();
|
let part = part.trim();
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match part {
|
match part {
|
||||||
"ctrl" | "control" => ctrl = true,
|
"ctrl" | "control" => ctrl = true,
|
||||||
"alt" | "opt" | "option" => alt = true,
|
"alt" | "opt" | "option" => alt = true,
|
||||||
|
|
@ -164,6 +167,18 @@ pub struct UserKeybindings {
|
||||||
pub bindings: Vec<UserBinding>,
|
pub bindings: Vec<UserBinding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct JsonKeybindingConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
bindings: Vec<JsonKeybindingBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct JsonKeybindingBlock {
|
||||||
|
context: String,
|
||||||
|
bindings: HashMap<String, Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserBinding {
|
pub struct UserBinding {
|
||||||
pub chord: String, // e.g. "ctrl+k ctrl+d"
|
pub chord: String, // e.g. "ctrl+k ctrl+d"
|
||||||
|
|
@ -172,10 +187,16 @@ pub struct UserBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserKeybindings {
|
impl UserKeybindings {
|
||||||
|
pub fn from_json_str(content: &str) -> Self {
|
||||||
|
serde_json::from_str(content)
|
||||||
|
.or_else(|_| Self::from_block_config(content))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load(config_dir: &Path) -> Self {
|
pub fn load(config_dir: &Path) -> Self {
|
||||||
let path = config_dir.join("keybindings.json");
|
let path = config_dir.join("keybindings.json");
|
||||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||||
serde_json::from_str(&content).unwrap_or_default()
|
Self::from_json_str(&content)
|
||||||
} else {
|
} else {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
@ -187,6 +208,23 @@ impl UserKeybindings {
|
||||||
std::fs::write(path, json)?;
|
std::fs::write(path, json)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_block_config(content: &str) -> Result<Self, serde_json::Error> {
|
||||||
|
let config: JsonKeybindingConfig = serde_json::from_str(content)?;
|
||||||
|
let bindings = config
|
||||||
|
.bindings
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|block| {
|
||||||
|
let context = block.context;
|
||||||
|
block.bindings.into_iter().map(move |(chord, action)| UserBinding {
|
||||||
|
chord,
|
||||||
|
action,
|
||||||
|
context: Some(context.clone()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(Self { bindings })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolved keybindings (defaults merged with user overrides)
|
/// Resolved keybindings (defaults merged with user overrides)
|
||||||
|
|
@ -420,4 +458,28 @@ mod tests {
|
||||||
let user = UserKeybindings::default();
|
let user = UserKeybindings::default();
|
||||||
assert!(user.bindings.is_empty());
|
assert!(user.bindings.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_keybindings_supports_ts_block_format() {
|
||||||
|
let user = UserKeybindings::from_json_str(
|
||||||
|
r#"{
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"context": "Chat",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl+g": "chat:externalEditor",
|
||||||
|
"space": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(user.bindings.len(), 2);
|
||||||
|
assert_eq!(user.bindings[0].context.as_deref(), Some("Chat"));
|
||||||
|
assert_eq!(user.bindings[0].chord, "ctrl+g");
|
||||||
|
assert_eq!(user.bindings[0].action.as_deref(), Some("chat:externalEditor"));
|
||||||
|
assert_eq!(user.bindings[1].chord, "space");
|
||||||
|
assert_eq!(user.bindings[1].action, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,8 @@ pub mod config {
|
||||||
pub max_tokens: Option<u32>,
|
pub max_tokens: Option<u32>,
|
||||||
pub permission_mode: PermissionMode,
|
pub permission_mode: PermissionMode,
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
|
#[serde(default)]
|
||||||
|
pub output_style: Option<String>,
|
||||||
pub auto_compact: bool,
|
pub auto_compact: bool,
|
||||||
pub compact_threshold: f32,
|
pub compact_threshold: f32,
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
|
@ -424,6 +426,8 @@ pub mod config {
|
||||||
pub append_system_prompt: Option<String>,
|
pub append_system_prompt: Option<String>,
|
||||||
pub disable_claude_mds: bool,
|
pub disable_claude_mds: bool,
|
||||||
pub project_dir: Option<PathBuf>,
|
pub project_dir: Option<PathBuf>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub workspace_paths: Vec<PathBuf>,
|
||||||
/// Event hooks: map of event → list of hook commands.
|
/// Event hooks: map of event → list of hook commands.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hooks: HashMap<HookEvent, Vec<HookEntry>>,
|
pub hooks: HashMap<HookEvent, Vec<HookEntry>>,
|
||||||
|
|
@ -518,6 +522,14 @@ pub mod config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the effective output style for system-prompt assembly.
|
||||||
|
pub fn effective_output_style(&self) -> crate::system_prompt::OutputStyle {
|
||||||
|
self.output_style
|
||||||
|
.as_deref()
|
||||||
|
.map(crate::system_prompt::OutputStyle::from_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve the API key from the config, then from `ANTHROPIC_API_KEY`.
|
/// Resolve the API key from the config, then from `ANTHROPIC_API_KEY`.
|
||||||
pub fn resolve_api_key(&self) -> Option<String> {
|
pub fn resolve_api_key(&self) -> Option<String> {
|
||||||
self.api_key
|
self.api_key
|
||||||
|
|
@ -633,6 +645,28 @@ pub mod config {
|
||||||
tokio::fs::write(&path, content).await?;
|
tokio::fs::write(&path, content).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Synchronous variant used by pre-session commands.
|
||||||
|
pub fn load_sync() -> anyhow::Result<Self> {
|
||||||
|
let path = Self::global_settings_path();
|
||||||
|
if path.exists() {
|
||||||
|
let content = std::fs::read_to_string(&path)?;
|
||||||
|
Ok(serde_json::from_str(&content).unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
Ok(Self::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronous variant used by pre-session commands.
|
||||||
|
pub fn save_sync(&self) -> anyhow::Result<()> {
|
||||||
|
let path = Self::global_settings_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let content = serde_json::to_string_pretty(self)?;
|
||||||
|
std::fs::write(&path, content)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ pub fn format_memory_manifest(memories: &[MemoryFileMeta]) -> String {
|
||||||
|
|
||||||
match &m.description {
|
match &m.description {
|
||||||
Some(desc) => format!("- {}{} ({}): {}", tag, m.filename, ts, desc),
|
Some(desc) => format!("- {}{} ({}): {}", tag, m.filename, ts, desc),
|
||||||
None => format!("- {}{} ({})", tag, m.filename, ts),
|
None => format!("- {}{}", tag, m.filename),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|
@ -361,8 +361,7 @@ pub fn auto_memory_path(project_root: &Path) -> PathBuf {
|
||||||
/// Sanitize an arbitrary string into a directory-name-safe component.
|
/// Sanitize an arbitrary string into a directory-name-safe component.
|
||||||
/// Matches `sanitizePath` used inside `getAutoMemPath` in `paths.ts`.
|
/// Matches `sanitizePath` used inside `getAutoMemPath` in `paths.ts`.
|
||||||
pub fn sanitize_path_component(s: &str) -> String {
|
pub fn sanitize_path_component(s: &str) -> String {
|
||||||
let sanitized: String = s
|
s.chars()
|
||||||
.chars()
|
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
|
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
|
||||||
c
|
c
|
||||||
|
|
@ -370,8 +369,7 @@ pub fn sanitize_path_component(s: &str) -> String {
|
||||||
'_'
|
'_'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect()
|
||||||
sanitized.trim_matches('_').to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the auto-memory system is enabled for this session.
|
/// Whether the auto-memory system is enabled for this session.
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ pub struct QueryConfig {
|
||||||
pub max_turns: u32,
|
pub max_turns: u32,
|
||||||
pub system_prompt: Option<String>,
|
pub system_prompt: Option<String>,
|
||||||
pub append_system_prompt: Option<String>,
|
pub append_system_prompt: Option<String>,
|
||||||
|
pub output_style: cc_core::system_prompt::OutputStyle,
|
||||||
|
pub working_directory: Option<String>,
|
||||||
pub thinking_budget: Option<u32>,
|
pub thinking_budget: Option<u32>,
|
||||||
pub temperature: Option<f32>,
|
pub temperature: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +73,8 @@ impl Default for QueryConfig {
|
||||||
max_turns: cc_core::constants::MAX_TURNS_DEFAULT,
|
max_turns: cc_core::constants::MAX_TURNS_DEFAULT,
|
||||||
system_prompt: None,
|
system_prompt: None,
|
||||||
append_system_prompt: None,
|
append_system_prompt: None,
|
||||||
|
output_style: cc_core::system_prompt::OutputStyle::Default,
|
||||||
|
working_directory: None,
|
||||||
thinking_budget: None,
|
thinking_budget: None,
|
||||||
temperature: None,
|
temperature: None,
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +86,11 @@ impl QueryConfig {
|
||||||
Self {
|
Self {
|
||||||
model: cfg.effective_model().to_string(),
|
model: cfg.effective_model().to_string(),
|
||||||
max_tokens: cfg.effective_max_tokens(),
|
max_tokens: cfg.effective_max_tokens(),
|
||||||
|
output_style: cfg.effective_output_style(),
|
||||||
|
working_directory: cfg
|
||||||
|
.project_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.display().to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -440,13 +449,12 @@ fn build_system_prompt(config: &QueryConfig) -> SystemPrompt {
|
||||||
custom_system_prompt: config.system_prompt.clone(),
|
custom_system_prompt: config.system_prompt.clone(),
|
||||||
append_system_prompt: config.append_system_prompt.clone(),
|
append_system_prompt: config.append_system_prompt.clone(),
|
||||||
// All other fields use sensible defaults:
|
// All other fields use sensible defaults:
|
||||||
// - prefix: auto-detect from env
|
// - prefix: auto-detect from env
|
||||||
// - output_style: Default (no suffix)
|
// - memory_content: empty (callers inject via append if needed)
|
||||||
// - working_directory: None (callers inject via append if needed)
|
|
||||||
// - memory_content: empty (callers inject via append if needed)
|
|
||||||
// - replace_system_prompt: false (additive mode)
|
// - replace_system_prompt: false (additive mode)
|
||||||
// - coordinator_mode: false
|
// - coordinator_mode: false
|
||||||
output_style: OutputStyle::Default,
|
output_style: config.output_style,
|
||||||
|
working_directory: config.working_directory.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -549,8 +549,9 @@ mod tests {
|
||||||
fn expand_prompt_suffix_empty() {
|
fn expand_prompt_suffix_empty() {
|
||||||
let skill = find_bundled_skill("stuck").unwrap();
|
let skill = find_bundled_skill("stuck").unwrap();
|
||||||
let expanded = expand_prompt(skill, "");
|
let expanded = expand_prompt(skill, "");
|
||||||
// $ARGUMENTS_SUFFIX should be "" (not ": ")
|
// $ARGUMENTS_SUFFIX should expand to "" so "stuck" is not followed by ": "
|
||||||
assert!(!expanded.contains(": "));
|
assert!(!expanded.contains("stuck: "));
|
||||||
|
assert!(!expanded.contains("$ARGUMENTS_SUFFIX"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue