Skip to content

Commit f4bd00e

Browse files
committed
✨ Add progressive diff analysis and custom instructions support
Change git_diff to return summaries by default, with optional file filtering and full diffs via detail='standard'. This progressive approach helps agents manage context more efficiently for large changesets. Add custom instructions parameter to execute_task_with_style, allowing users to provide additional guidance when generating commits and other artifacts. Other improvements: - Make workspace persistent across agent invocations - Add 2-minute timeout and preserved ordering for parallel_analyze subagents - Move semantic blame I/O to background task to avoid blocking UI - Fix string truncation to use char boundaries (Unicode safety) - Fix status bar overflow on narrow terminals - Display link URLs in chat markdown rendering - Return proper error exit code from Studio on errors
1 parent 6069a03 commit f4bd00e

File tree

10 files changed

+380
-197
lines changed

10 files changed

+380
-197
lines changed

src/agents/capabilities/commit.toml

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,26 @@ Do not skip this step.
1212
1313
## Tools Available
1414
- `project_docs(doc_type="context")` - **CALL FIRST** - Get README + AGENTS.md/CLAUDE.md for project context
15-
- `git_diff()` - Get staged changes with relevance scores and size guidance
15+
- `git_diff()` - Get summary of staged changes with relevance scores (default: summary only)
16+
- `git_diff(detail="standard", files=["path1","path2"])` - Get full diffs for specific files
1617
- `git_log(count=5)` - Recent commits for style reference
1718
- `project_metadata()` - Get project language, framework, and dependencies (optional)
1819
- `parallel_analyze(tasks=[...])` - Spawn subagents for very large changesets (optional)
1920
20-
## Workflow
21+
## Workflow — Progressive Analysis
2122
1. **FIRST**: Call `project_docs(doc_type="context")` — understand the project's conventions
22-
2. Call `git_diff()` to see what changed
23-
3. Read the **Size** and **Guidance** in the summary header
24-
4. Follow the guidance for focusing on relevant files
25-
5. Generate the commit message following any project-specific conventions from step 1
23+
2. Call `git_diff()` to get the **summary** (file list with relevance scores, no diffs yet)
24+
3. Review the summary to identify important files (highest relevance scores)
25+
4. Call `git_diff(detail="standard", files=["important-file1.rs", "important-file2.rs"])` for full diffs of key files
26+
5. Repeat step 4 for additional files if needed (stay focused on top 5-7 files max)
27+
6. Generate the commit message based on your progressive analysis
28+
29+
**CRITICAL**: Never request all diffs at once for large changesets. Always start with summary, then selectively drill into important files.
2630
2731
## Context Strategy by Size
28-
- **Small** (≤3 files, <100 lines): Consider all changes equally
29-
- **Medium** (≤10 files, <500 lines): Focus on files with >60% relevance
30-
- **Large** (>10 files or >500 lines): Focus ONLY on top 5-7 highest-relevance files; summarize the theme of lower-relevance changes
32+
- **Small** (≤3 files, <100 lines): Can use `git_diff(detail="standard")` to see all diffs
33+
- **Medium** (≤10 files, <500 lines): Start with summary, then get diffs for >60% relevance files
34+
- **Large** (>10 files or >500 lines): Summary first, then analyze top 5-7 files individually
3135
- **Very Large** (>20 files or >1000 lines): Use `parallel_analyze` to distribute analysis:
3236
- Example: `parallel_analyze({ "tasks": ["Summarize changes in src/api/", "Summarize changes in src/models/", "Summarize infrastructure changes"] })`
3337
- Each subagent analyzes its scope independently

src/agents/iris.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,8 @@ pub struct IrisAgent {
327327
config: Option<crate::config::Config>,
328328
/// Optional sender for content updates (used in Studio chat mode)
329329
content_update_sender: Option<crate::agents::tools::ContentUpdateSender>,
330+
/// Persistent workspace for notes and task tracking (shared across agent invocations)
331+
workspace: Workspace,
330332
}
331333

332334
impl IrisAgent {
@@ -341,6 +343,7 @@ impl IrisAgent {
341343
preamble: None,
342344
config: None,
343345
content_update_sender: None,
346+
workspace: Workspace::new(),
344347
})
345348
}
346349

@@ -433,8 +436,8 @@ Guidelines:
433436
// Attach core tools (shared with subagents) + GitRepoInfo (main agent only)
434437
let agent_builder = crate::attach_core_tools!(agent_builder)
435438
.tool(DebugTool::new(GitRepoInfo))
436-
// Workspace for Iris's notes and task management
437-
.tool(DebugTool::new(Workspace::new()))
439+
// Workspace for Iris's notes and task management (clone to share Arc-backed state)
440+
.tool(DebugTool::new(self.workspace.clone()))
438441
// Parallel analysis for distributing work across multiple subagents
439442
.tool(DebugTool::new(ParallelAnalyze::new(
440443
&self.provider,

src/agents/setup.rs

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ impl IrisAgentService {
244244
// Create the agent
245245
let mut agent = self.create_agent()?;
246246

247-
// Build task prompt with context information
248-
let task_prompt = Self::build_task_prompt(capability, &context);
247+
// Build task prompt with context information (no custom instructions)
248+
let task_prompt = Self::build_task_prompt(capability, &context, None);
249249

250250
// Execute the task
251251
agent.execute_task(capability, &task_prompt).await
@@ -272,12 +272,14 @@ impl IrisAgentService {
272272
/// * `context` - Structured context describing what to analyze
273273
/// * `preset` - Optional preset name override (e.g., "conventional", "cosmic")
274274
/// * `use_gitmoji` - Optional gitmoji setting override
275+
/// * `instructions` - Optional custom instructions from the user
275276
pub async fn execute_task_with_style(
276277
&self,
277278
capability: &str,
278279
context: TaskContext,
279280
preset: Option<&str>,
280281
use_gitmoji: Option<bool>,
282+
instructions: Option<&str>,
281283
) -> Result<StructuredResponse> {
282284
// Clone config and apply style overrides
283285
let mut config = self.config.clone();
@@ -296,42 +298,52 @@ impl IrisAgentService {
296298
agent.set_config(config);
297299
agent.set_fast_model(self.fast_model.clone());
298300

299-
// Build task prompt with context information
300-
let task_prompt = Self::build_task_prompt(capability, &context);
301+
// Build task prompt with context information and optional instructions
302+
let task_prompt = Self::build_task_prompt(capability, &context, instructions);
301303

302304
// Execute the task
303305
agent.execute_task(capability, &task_prompt).await
304306
}
305307

306-
/// Build a task prompt incorporating the context information
307-
fn build_task_prompt(capability: &str, context: &TaskContext) -> String {
308+
/// Build a task prompt incorporating the context information and optional instructions
309+
fn build_task_prompt(
310+
capability: &str,
311+
context: &TaskContext,
312+
instructions: Option<&str>,
313+
) -> String {
308314
let context_json = context.to_prompt_context();
309315
let diff_hint = context.diff_hint();
310316

317+
// Build instruction suffix if provided
318+
let instruction_suffix = instructions
319+
.filter(|i| !i.trim().is_empty())
320+
.map(|i| format!("\n\n## Custom Instructions\n{}", i))
321+
.unwrap_or_default();
322+
311323
match capability {
312324
"commit" => format!(
313-
"Generate a commit message for the following context:\n{}\n\nUse: {}",
314-
context_json, diff_hint
325+
"Generate a commit message for the following context:\n{}\n\nUse: {}{}",
326+
context_json, diff_hint, instruction_suffix
315327
),
316328
"review" => format!(
317-
"Review the code changes for the following context:\n{}\n\nUse: {}",
318-
context_json, diff_hint
329+
"Review the code changes for the following context:\n{}\n\nUse: {}{}",
330+
context_json, diff_hint, instruction_suffix
319331
),
320332
"pr" => format!(
321-
"Generate a pull request description for:\n{}\n\nUse: {}",
322-
context_json, diff_hint
333+
"Generate a pull request description for:\n{}\n\nUse: {}{}",
334+
context_json, diff_hint, instruction_suffix
323335
),
324336
"changelog" => format!(
325-
"Generate a changelog for:\n{}\n\nUse: {}",
326-
context_json, diff_hint
337+
"Generate a changelog for:\n{}\n\nUse: {}{}",
338+
context_json, diff_hint, instruction_suffix
327339
),
328340
"release_notes" => format!(
329-
"Generate release notes for:\n{}\n\nUse: {}",
330-
context_json, diff_hint
341+
"Generate release notes for:\n{}\n\nUse: {}{}",
342+
context_json, diff_hint, instruction_suffix
331343
),
332344
_ => format!(
333-
"Execute task with context:\n{}\n\nHint: {}",
334-
context_json, diff_hint
345+
"Execute task with context:\n{}\n\nHint: {}{}",
346+
context_json, diff_hint, instruction_suffix
335347
),
336348
}
337349
}
@@ -412,7 +424,7 @@ impl IrisAgentService {
412424
F: FnMut(&str, &str) + Send,
413425
{
414426
let mut agent = self.create_agent()?;
415-
let task_prompt = Self::build_task_prompt(capability, &context);
427+
let task_prompt = Self::build_task_prompt(capability, &context, None);
416428
agent
417429
.execute_task_streaming(capability, &task_prompt, on_chunk)
418430
.await

src/agents/tools/docs.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,14 @@ impl Tool for ProjectDocs {
140140

141141
output.push_str(&format!("=== {} ===\n", filename));
142142

143-
// Truncate if too long
144-
if content.len() > max_chars {
145-
output.push_str(&content[..max_chars]);
143+
// Truncate if too long (use char boundary-safe truncation)
144+
let char_count = content.chars().count();
145+
if char_count > max_chars {
146+
let truncated: String = content.chars().take(max_chars).collect();
147+
output.push_str(&truncated);
146148
output.push_str(&format!(
147149
"\n\n[... truncated, {} more chars ...]\n",
148-
content.len() - max_chars
150+
char_count - max_chars
149151
));
150152
} else {
151153
output.push_str(&content);

0 commit comments

Comments
 (0)