Skip to content

Commit bebffef

Browse files
committed
✨ Add shell completions, configurable subagent timeout, and release notes file update
Introduce shell completion script generation for bash, zsh, fish, elvish, and powershell via the new `completions` subcommand. Add configurable timeout for parallel subagent tasks via `--subagent-timeout` flag in both global and project config commands. The timeout defaults to 120 seconds and is now stored in the config file. Extend the release-notes command with `--update` and `--file` flags to automatically write generated release notes to a file (prepending to existing content or creating a new file). Improve gitmoji CLI handling by replacing the single `--gitmoji` flag with explicit `--gitmoji` and `--no-gitmoji` flags for clearer intent. Refactor project-config command to properly isolate project settings from personal config, ensuring API keys are never inherited into project files.
1 parent 50810c3 commit bebffef

File tree

8 files changed

+376
-69
lines changed

8 files changed

+376
-69
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ arboard = "3.4"
2828
async-trait = "0.1.88"
2929
chrono = { version = "0.4.38", features = ["serde"] }
3030
clap = { version = "4.5.36", features = ["derive", "cargo"] }
31+
clap_complete = "4.5"
3132
colored = "3.0.0"
3233
crossterm = "0.28.1"
3334
dirs = "6.0.0"

src/agents/iris.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,9 +439,12 @@ Guidelines:
439439
// Workspace for Iris's notes and task management (clone to share Arc-backed state)
440440
.tool(DebugTool::new(self.workspace.clone()))
441441
// Parallel analysis for distributing work across multiple subagents
442-
.tool(DebugTool::new(ParallelAnalyze::new(
442+
.tool(DebugTool::new(ParallelAnalyze::with_timeout(
443443
&self.provider,
444444
fast_model,
445+
self.config
446+
.as_ref()
447+
.map_or(120, |c| c.subagent_timeout_secs),
445448
)))
446449
// Sub-agent delegation (Rig's built-in agent-as-tool!)
447450
.tool(sub_agent);

src/agents/tools/parallel_analyze.rs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ use tokio::sync::Mutex;
2020

2121
use crate::agents::debug as agent_debug;
2222

23-
/// Timeout for individual subagent tasks (2 minutes)
24-
const SUBAGENT_TIMEOUT: Duration = Duration::from_secs(120);
23+
/// Default timeout for individual subagent tasks (2 minutes)
24+
const DEFAULT_SUBAGENT_TIMEOUT_SECS: u64 = 120;
2525

2626
/// Arguments for parallel analysis
2727
#[derive(Debug, Deserialize, JsonSchema)]
@@ -140,10 +140,18 @@ impl SubagentRunner {
140140
pub struct ParallelAnalyze {
141141
runner: SubagentRunner,
142142
model: String,
143+
/// Timeout in seconds for each subagent task
144+
timeout_secs: u64,
143145
}
144146

145147
impl ParallelAnalyze {
148+
/// Create a new parallel analyzer with default timeout
146149
pub fn new(provider: &str, model: &str) -> Self {
150+
Self::with_timeout(provider, model, DEFAULT_SUBAGENT_TIMEOUT_SECS)
151+
}
152+
153+
/// Create a new parallel analyzer with custom timeout
154+
pub fn with_timeout(provider: &str, model: &str, timeout_secs: u64) -> Self {
147155
// Default to openai if creation fails
148156
let runner = SubagentRunner::new(provider, model).unwrap_or_else(|_| {
149157
tracing::warn!(
@@ -156,6 +164,7 @@ impl ParallelAnalyze {
156164
Self {
157165
runner,
158166
model: model.to_string(),
167+
timeout_secs,
159168
}
160169
}
161170
}
@@ -220,25 +229,25 @@ impl Tool for ParallelAnalyze {
220229

221230
// Spawn all tasks as parallel tokio tasks, tracking index for ordering
222231
let mut handles = Vec::new();
232+
let timeout = Duration::from_secs(self.timeout_secs);
223233
for (index, task) in tasks.into_iter().enumerate() {
224234
let runner = self.runner.clone();
225235
let results = Arc::clone(&results);
236+
let task_timeout = timeout;
237+
let timeout_secs = self.timeout_secs;
226238

227239
let handle = tokio::spawn(async move {
228240
// Wrap task execution in timeout to prevent hanging
229-
let result =
230-
match tokio::time::timeout(SUBAGENT_TIMEOUT, runner.run_task(&task)).await {
231-
Ok(result) => result,
232-
Err(_) => SubagentResult {
233-
task: task.clone(),
234-
result: String::new(),
235-
success: false,
236-
error: Some(format!(
237-
"Task timed out after {} seconds",
238-
SUBAGENT_TIMEOUT.as_secs()
239-
)),
240-
},
241-
};
241+
let result = match tokio::time::timeout(task_timeout, runner.run_task(&task)).await
242+
{
243+
Ok(result) => result,
244+
Err(_) => SubagentResult {
245+
task: task.clone(),
246+
result: String::new(),
247+
success: false,
248+
error: Some(format!("Task timed out after {} seconds", timeout_secs)),
249+
},
250+
};
242251

243252
// Store result at original index to preserve ordering
244253
let mut guard = results.lock().await;

src/cli.rs

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ use crate::providers::Provider;
55
use crate::theme;
66
use crate::ui;
77
use clap::builder::{Styles, styling::AnsiColor};
8-
use clap::{Parser, Subcommand, crate_version};
8+
use clap::{CommandFactory, Parser, Subcommand, crate_version};
9+
use clap_complete::{Shell, generate};
910
use colored::Colorize;
11+
use std::io;
1012

1113
/// Default log file path for debug output
1214
pub const LOG_FILE: &str = "git-iris-debug.log";
@@ -260,6 +262,17 @@ pub enum Commands {
260262
#[arg(long, help = "Output raw markdown without any console formatting")]
261263
raw: bool,
262264

265+
/// Update the release notes file with the new content
266+
#[arg(long, help = "Update the release notes file with the new content")]
267+
update: bool,
268+
269+
/// Path to the release notes file
270+
#[arg(
271+
long,
272+
help = "Path to the release notes file (defaults to RELEASE_NOTES.md)"
273+
)]
274+
file: Option<String>,
275+
263276
/// Explicit version name to use in the release notes instead of getting it from Git
264277
#[arg(long, help = "Explicit version name to use in the release notes")]
265278
version_name: Option<String>,
@@ -323,6 +336,13 @@ pub enum Commands {
323336
help = "Set additional parameters for the specified provider (key=value)"
324337
)]
325338
param: Option<Vec<String>>,
339+
340+
/// Set timeout in seconds for parallel subagent tasks
341+
#[arg(
342+
long,
343+
help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
344+
)]
345+
subagent_timeout: Option<u64>,
326346
},
327347

328348
/// Create or update a project-specific configuration file
@@ -356,6 +376,13 @@ pub enum Commands {
356376
)]
357377
param: Option<Vec<String>>,
358378

379+
/// Set timeout in seconds for parallel subagent tasks
380+
#[arg(
381+
long,
382+
help = "Set timeout in seconds for parallel subagent tasks (default: 120)"
383+
)]
384+
subagent_timeout: Option<u64>,
385+
359386
/// Print the current project configuration
360387
#[arg(short, long, help = "Print the current project configuration")]
361388
print: bool,
@@ -368,6 +395,17 @@ pub enum Commands {
368395
/// List available themes
369396
#[command(about = "List available themes")]
370397
Themes,
398+
399+
/// Generate shell completions
400+
#[command(
401+
about = "Generate shell completions",
402+
long_about = "Generate shell completion scripts for bash, zsh, fish, elvish, or powershell.\n\nUsage examples:\n• Bash: git-iris completions bash >> ~/.bashrc\n• Zsh: git-iris completions zsh >> ~/.zshrc\n• Fish: git-iris completions fish > ~/.config/fish/completions/git-iris.fish"
403+
)]
404+
Completions {
405+
/// Shell to generate completions for
406+
#[arg(value_enum)]
407+
shell: Shell,
408+
},
371409
}
372410

373411
/// Define custom styles for Clap
@@ -674,16 +712,26 @@ fn handle_config(
674712
fast_model: Option<String>,
675713
token_limit: Option<usize>,
676714
param: Option<Vec<String>>,
715+
subagent_timeout: Option<u64>,
677716
) -> anyhow::Result<()> {
678717
log_debug!(
679-
"Handling 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}",
718+
"Handling 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}, subagent_timeout: {:?}",
680719
common,
681720
api_key,
682721
model,
683722
token_limit,
684-
param
723+
param,
724+
subagent_timeout
685725
);
686-
commands::handle_config_command(common, api_key, model, fast_model, token_limit, param)
726+
commands::handle_config_command(
727+
common,
728+
api_key,
729+
model,
730+
fast_model,
731+
token_limit,
732+
param,
733+
subagent_timeout,
734+
)
687735
}
688736

689737
/// Handle the `Review` command
@@ -852,20 +900,25 @@ async fn handle_changelog(
852900
}
853901

854902
/// Handle the `Release Notes` command
903+
#[allow(clippy::too_many_arguments)]
855904
async fn handle_release_notes(
856905
common: CommonParams,
857906
from: String,
858907
to: Option<String>,
859908
raw: bool,
860909
repository_url: Option<String>,
910+
update: bool,
911+
file: Option<String>,
861912
_version_name: Option<String>,
862913
) -> anyhow::Result<()> {
863914
log_debug!(
864-
"Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, raw: {}",
915+
"Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, raw: {}, update: {}, file: {:?}",
865916
common,
866917
from,
867918
to,
868-
raw
919+
raw,
920+
update,
921+
file
869922
);
870923

871924
// For raw output, skip all formatting
@@ -875,6 +928,8 @@ async fn handle_release_notes(
875928
}
876929

877930
use crate::agents::{IrisAgentService, TaskContext};
931+
use std::fs;
932+
use std::path::Path;
878933

879934
// Create structured context for release notes
880935
let context = TaskContext::for_changelog(from, to);
@@ -896,6 +951,43 @@ async fn handle_release_notes(
896951
}
897952

898953
println!("{response}");
954+
955+
// Handle --update flag
956+
if update {
957+
let release_notes_path = file.unwrap_or_else(|| "RELEASE_NOTES.md".to_string());
958+
let formatted_content = response.to_string();
959+
960+
let update_spinner = ui::create_spinner(&format!(
961+
"Updating release notes file at {release_notes_path}..."
962+
));
963+
964+
// Write or append to file
965+
let path = Path::new(&release_notes_path);
966+
let result = if path.exists() {
967+
// Prepend to existing file
968+
let existing = fs::read_to_string(path)?;
969+
fs::write(path, format!("{formatted_content}\n\n---\n\n{existing}"))
970+
} else {
971+
// Create new file
972+
fs::write(path, &formatted_content)
973+
};
974+
975+
match result {
976+
Ok(()) => {
977+
update_spinner.finish_and_clear();
978+
ui::print_success(&format!(
979+
"✨ Release notes successfully updated at {}",
980+
release_notes_path.bright_green()
981+
));
982+
}
983+
Err(e) => {
984+
update_spinner.finish_and_clear();
985+
ui::print_error(&format!("Failed to update release notes file: {e}"));
986+
return Err(e.into());
987+
}
988+
}
989+
}
990+
899991
Ok(())
900992
}
901993

@@ -934,7 +1026,16 @@ pub async fn handle_command(
9341026
fast_model,
9351027
token_limit,
9361028
param,
937-
} => handle_config(&common, api_key, model, fast_model, token_limit, param),
1029+
subagent_timeout,
1030+
} => handle_config(
1031+
&common,
1032+
api_key,
1033+
model,
1034+
fast_model,
1035+
token_limit,
1036+
param,
1037+
subagent_timeout,
1038+
),
9381039
Commands::Review {
9391040
common,
9401041
print,
@@ -982,28 +1083,48 @@ pub async fn handle_command(
9821083
from,
9831084
to,
9841085
raw,
1086+
update,
1087+
file,
9851088
version_name,
986-
} => handle_release_notes(common, from, to, raw, repository_url, version_name).await,
1089+
} => {
1090+
handle_release_notes(
1091+
common,
1092+
from,
1093+
to,
1094+
raw,
1095+
repository_url,
1096+
update,
1097+
file,
1098+
version_name,
1099+
)
1100+
.await
1101+
}
9871102
Commands::ProjectConfig {
9881103
common,
9891104
model,
9901105
fast_model,
9911106
token_limit,
9921107
param,
1108+
subagent_timeout,
9931109
print,
9941110
} => commands::handle_project_config_command(
9951111
&common,
9961112
model,
9971113
fast_model,
9981114
token_limit,
9991115
param,
1116+
subagent_timeout,
10001117
print,
10011118
),
10021119
Commands::ListPresets => commands::handle_list_presets_command(),
10031120
Commands::Themes => {
10041121
handle_themes();
10051122
Ok(())
10061123
}
1124+
Commands::Completions { shell } => {
1125+
handle_completions(shell);
1126+
Ok(())
1127+
}
10071128
Commands::Pr {
10081129
common,
10091130
print,
@@ -1104,6 +1225,12 @@ fn handle_themes() {
11041225
);
11051226
}
11061227

1228+
/// Handle the `Completions` command - generate shell completion scripts
1229+
fn handle_completions(shell: Shell) {
1230+
let mut cmd = Cli::command();
1231+
generate(shell, &mut cmd, "git-iris", &mut io::stdout());
1232+
}
1233+
11071234
/// Handle the `Pr` command with agent framework
11081235
async fn handle_pr_with_agent(
11091236
common: CommonParams,

0 commit comments

Comments
 (0)