Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 170 additions & 158 deletions src/agent/mod.rs

Large diffs are not rendered by default.

31 changes: 16 additions & 15 deletions src/agent/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tinyharness_lib::{
tools::ToolManager,
};

use crate::commands::CommandContext;
use crate::commands::compact::execute_compact;
use crate::style::*;
use crate::ui::confirm::Confirmation;
Expand All @@ -27,7 +28,7 @@ pub async fn handle_tool_calls<W: Write>(
response_content: &str,
messages: &mut Vec<Message>,
tool_manager: &ToolManager,
dispatcher: &mut crate::commands::CommandDispatcher,
ctx: &mut CommandContext,
stdout: &mut W,
auto_accept: &mut bool,
session: &mut Session,
Expand Down Expand Up @@ -74,7 +75,7 @@ pub async fn handle_tool_calls<W: Write>(
{
match event {
SignalEvent::SwitchMode { mode } => {
handle_switch_mode(mode, dispatcher, messages, session, stdout)?;
handle_switch_mode(mode, ctx, messages, session, stdout)?;
}
SignalEvent::Question { question, answers } => {
handle_question(&question, &answers, messages, session, stdout)?;
Expand All @@ -90,17 +91,17 @@ pub async fn handle_tool_calls<W: Write>(
.await?;
}
SignalEvent::InvokeSkill { skill_name } => {
// Clone skill info to avoid borrowing dispatcher while calling it mutably
// Clone skill info to avoid borrowing ctx while calling it mutably
let skill_result = {
let registry = &dispatcher.skill_registry;
let registry = &ctx.skill_registry;
registry
.get(&skill_name)
.map(|s| (s.name.clone(), s.description.clone()))
};
handle_invoke_skill(
&skill_name,
&skill_result,
dispatcher,
ctx,
messages,
session,
stdout,
Expand Down Expand Up @@ -450,16 +451,16 @@ async fn execute_generic_tool<W: Write>(
session.append_message(messages.last().expect("just pushed a message"));
}

/// Handle the switch_mode signal: update dispatcher and system prompt.
/// Handle the switch_mode signal: update context and system prompt.
fn handle_switch_mode<W: Write>(
new_mode: AgentMode,
dispatcher: &mut crate::commands::CommandDispatcher,
ctx: &mut CommandContext,
messages: &mut Vec<Message>,
session: &mut Session,
stdout: &mut W,
) -> Result<(), Box<dyn std::error::Error>> {
let old_mode = dispatcher.current_mode;
match dispatcher.switch_mode(new_mode, messages) {
let old_mode = ctx.current_mode;
match ctx.switch_mode(new_mode, messages) {
Ok(()) => {
session.set_mode(new_mode);

Expand Down Expand Up @@ -675,20 +676,20 @@ async fn handle_auto_compact<W: Write>(
/// system prompt to include the skill's instructions.
///
/// `skill_result` is `Some((name, description))` if the skill was found,
/// or `None` if not found. This avoids borrowing the dispatcher while also
/// or `None` if not found. This avoids borrowing the context while also
/// calling it mutably.
fn handle_invoke_skill<W: Write>(
skill_name: &str,
skill_result: &Option<(String, String)>,
dispatcher: &mut crate::commands::CommandDispatcher,
ctx: &mut CommandContext,
messages: &mut Vec<Message>,
session: &mut Session,
stdout: &mut W,
) -> Result<(), Box<dyn std::error::Error>> {
match skill_result {
Some((name, description)) => {
// Prevent duplicate activation
if dispatcher
if ctx
.active_skills
.iter()
.any(|s| s.eq_ignore_ascii_case(name))
Expand Down Expand Up @@ -720,7 +721,7 @@ fn handle_invoke_skill<W: Write>(
stdout.flush()?;

// Track the active skill
dispatcher.active_skills.push(name.clone());
ctx.active_skills.push(name.clone());

messages.push(Message {
role: Role::Tool,
Expand All @@ -733,10 +734,10 @@ fn handle_invoke_skill<W: Write>(
session.append_message(messages.last().expect("just pushed a message"));

// Refresh system prompt to include the active skill
dispatcher.refresh_system_prompt(messages);
ctx.refresh_system_prompt(messages);
}
None => {
let available = dispatcher
let available = ctx
.skill_registry
.skills
.iter()
Expand Down
39 changes: 38 additions & 1 deletion src/commands/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ use std::collections::HashSet;

use tinyharness_lib::config::{get_default_safe_commands, load_settings, save_settings};

use crate::commands::registry::CommandResult;
use crate::style::*;

// ── Core implementation ─────────────────────────────────────────────────────

/// Check if a command prefix would match any command in the safe list.
fn matches_safe_prefix(cmd: &str, safe_commands: &[String]) -> bool {
for prefix in safe_commands {
Expand All @@ -17,6 +20,40 @@ fn matches_safe_prefix(cmd: &str, safe_commands: &[String]) -> bool {
false
}

pub fn execute(args: &str) -> Result<CommandResult, String> {
let args = args.trim();

if args.is_empty() {
execute_list();
return Ok(CommandResult::Ok);
}

// Split into subcommand + rest
let sub_parts: Vec<&str> = args.splitn(2, ' ').collect();
let sub = sub_parts[0].to_lowercase();
let cmd_arg = sub_parts
.get(1)
.map(|s| s.trim())
.unwrap_or("")
.trim_matches('"')
.trim_matches('\'')
.to_string();

match sub.as_str() {
"add" => execute_add(&cmd_arg),
"rm" | "remove" => execute_remove(&cmd_arg),
"deny" => execute_deny(&cmd_arg),
"undeny" | "allow" => execute_undeny(&cmd_arg),
"list" | "ls" => execute_list(),
"reset" => execute_reset(),
"resetdeny" => execute_reset_deny(),
"help" => execute_help(),
_ => execute_list(),
}

Ok(CommandResult::Ok)
}

pub fn execute_add(cmd: &str) {
if cmd.is_empty() {
println!(
Expand Down Expand Up @@ -201,7 +238,7 @@ pub fn format_denied_command_rows(cmds: &[&str]) -> Vec<String> {
}
line.push_str(&format!("{}✕ {}{}{}", RED, cmd, RESET, RESET));
}
rows.push(line);
rows.push(line)
}
rows
}
Expand Down
24 changes: 23 additions & 1 deletion src/commands/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,30 @@ use tinyharness_lib::{
token::{estimate_conversation_tokens, estimate_tokens},
};

use crate::async_command;
use crate::commands::registry::CommandResult;
use crate::style::*;

// ── Command trait implementation ────────────────────────────────────────────

async_command!(
CompactCommand,
"/compact",
"Summarize conversation history to free context space. Optionally specify a focus area.",
"/compact [focus]",
|raw_arg, ctx, messages| {
let focus = raw_arg.unwrap_or("").to_string();
let provider = ctx.provider.clone();
async move {
let mut p = provider.lock().await;
execute_compact(&mut *p, messages, &focus).await?;
Ok(CommandResult::Ok)
}
}
);

// ── Core implementation ─────────────────────────────────────────────────────

/// Maximum characters per message when formatting for summarization.
const MAX_CHARS_PER_MESSAGE: usize = 2000;

Expand Down Expand Up @@ -434,7 +456,7 @@ mod tests {

#[test]
fn test_format_messages_all_roles() {
let msgs = vec![
let msgs = [
Message {
role: Role::System,
content: "sys".to_string(),
Expand Down
Loading
Loading