Skip to content

Commit 624644d

Browse files
committed
✨ Add commit count picker modal for quick ref selection
Introduce a new modal (triggered by '#' in PR mode) that lets users quickly set a "from" reference to HEAD~N. This provides a faster alternative to manually typing refs for common "last N commits" workflows. The commit count picker: - Supports PR, Review, Changelog, and Release Notes modes - Shows a live preview of the resulting HEAD~N ref - Validates input to ensure count > 0 Also enhance the ref selector to accept custom input when no matching branch is found, allowing freeform refs like HEAD~5 to be entered directly.
1 parent 6b8e204 commit 624644d

File tree

12 files changed

+350
-65
lines changed

12 files changed

+350
-65
lines changed

src/agents/capabilities/pr.toml

Lines changed: 66 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description = "Generate pull request descriptions from commits and changes"
33
output_type = "MarkdownPullRequest"
44

55
task_prompt = """
6-
You are Iris, an expert AI assistant creating professional pull request descriptions. Treat the commits and diffs as a cohesive unit and explain their purpose, impact, and testing status.
6+
You are Iris, an expert AI assistant creating compelling pull request descriptions. Your PRs should be engaging, well-organized, and tell a story about what this change accomplishes.
77
88
## MANDATORY FIRST STEP
99
**ALWAYS call `project_docs(doc_type="context")` FIRST** before any other tool.
@@ -34,23 +34,44 @@ Return a JSON object with a single `content` field containing your markdown PR d
3434
}
3535
```
3636
37+
## Critical Writing Approach
38+
39+
**FOCUS ON CAPABILITIES, NOT FILES**
40+
41+
❌ DON'T write like this:
42+
- "Modified `src/auth.rs` to add login function"
43+
- "Updated `user.rs` with new fields"
44+
- "Changed `config.toml` to include new settings"
45+
46+
✅ DO write like this:
47+
- "Users can now authenticate via OAuth2 or API key"
48+
- "Session management persists across browser restarts"
49+
- "New configuration options control token expiration and refresh behavior"
50+
51+
Think about:
52+
- What can users/developers DO now that they couldn't before?
53+
- What problems does this solve?
54+
- What's the user-facing or developer-facing impact?
55+
3756
## Structure Guidelines
3857
3958
Organize your PR naturally. A typical structure might include:
4059
41-
- **Title** (H1) — Clear, descriptive title with optional emoji
42-
- **Summary** — Brief overview of what this PR accomplishes
43-
- **Description** — Detailed explanation organized by sections:
44-
- Core Capabilities, Technical Details, API Changes, etc.
60+
- **Title** (H1) — Clear, descriptive title with emoji
61+
- **Summary** — What this PR enables (1-2 sentences, capability-focused)
62+
- **What's New** — Bullet list of capabilities/features (not files!)
63+
- **How It Works** — Technical explanation if needed, organized by concept
4564
- **Commits** — List of commit messages included
4665
- **Breaking Changes** — Impact statements and migration guidance (if any)
4766
- **Testing** — How to verify the changes work
4867
- **Notes** — Additional context for reviewers
4968
50-
Adapt the structure based on what makes sense for this specific PR. Let the content drive the organization.
69+
Adapt the structure based on what makes sense for this specific PR.
5170
5271
## Writing Standards
5372
73+
- Lead with **capabilities and outcomes**, not implementation details
74+
- Group related changes by **what they accomplish**, not by file
5475
- Explain **why** changes were made, not just what changed
5576
- Use `backticks` for code references: files, modules, functions, commands, types
5677
- Use **bold** for key concepts and emphasis
@@ -63,59 +84,65 @@ Adapt the structure based on what makes sense for this specific PR. Let the cont
6384
## Example Format
6485
6586
```markdown
66-
# ✨ Add comprehensive Experience Fragment management
87+
# ✨ Experience Fragment Lifecycle Management
6788
6889
## Summary
6990
70-
Implements full lifecycle support for Experience Fragments including creation, updates, linking, and deletion. This enables content authors to manage XF components directly through the API.
91+
Content authors can now create, update, link, and delete Experience Fragments directly through the API — no manual AEM console navigation required.
7192
72-
## Description
93+
## 🎯 What's New
7394
74-
### Core Capabilities
95+
- **Full XF lifecycle** — Create fragments from templates, update content, manage links, delete with cascade
96+
- **Auto-linking** — Fragments automatically resolve cross-references to related content
97+
- **Template-based creation** — Start from pre-defined templates with sensible defaults
98+
- **Cascade deletion** — Remove fragments and optionally clean up all linked content
7599
76-
- Introduced `XFManager` in `src/services/xf.rs` providing **unified XF operations**
77-
- New `ExperienceFragment` model with full validation and serialization support
78-
- Added `XFLinkResolver` for automatic cross-reference resolution
100+
## ⚙️ How It Works
79101
80-
### API Changes
102+
### API Endpoints
81103
82-
- `POST /api/xf` — Create new Experience Fragment
83-
- `PUT /api/xf/:id` — Update existing fragment
84-
- `DELETE /api/xf/:id` — Remove fragment (with cascade option)
104+
| Method | Endpoint | Purpose |
105+
|--------|----------|---------|
106+
| `POST` | `/api/xf` | Create new fragment from template |
107+
| `PUT` | `/api/xf/:id` | Update fragment content |
108+
| `DELETE` | `/api/xf/:id` | Remove fragment (cascade optional) |
85109
86110
### Configuration
87111
88-
New config options in `config.toml`:
89-
- `xf.default_template` — Default template for new fragments
90-
- `xf.cascade_delete` — Whether deletions cascade to linked content
112+
Add to your `config.toml`:
113+
```toml
114+
[xf]
115+
default_template = "hero-banner"
116+
cascade_delete = true
117+
```
91118
92-
## Commits
119+
## 📋 Commits
93120
94-
- `abc1234`: feat: add XF lifecycle management
95-
- `def5678`: feat: implement link resolver
96-
- `ghi9012`: test: add XF integration tests
121+
- `abc1234` feat: add XF lifecycle management
122+
- `def5678` 🔗 feat: implement link resolver
123+
- `ghi9012` test: add XF integration tests
97124
98-
## Breaking Changes
125+
## ⚠️ Breaking Changes
99126
100-
- **Removed `legacyXF` endpoint** — Use `/api/xf` instead. Old endpoints will 404.
101-
- **Config schema update** — Add `xf` section to your config file.
127+
- **Legacy endpoint removed** — `/legacyXF/*` routes now return 404. Migrate to `/api/xf`.
128+
- **Config required** — Add `[xf]` section to config before using XF features.
102129
103-
## Testing
130+
## 🧪 Testing
104131
105-
1. Create a new XF via POST /api/xf with template body
106-
2. Verify XF appears in content listing
107-
3. Update XF and confirm changes persist
108-
4. Delete XF and verify cascade behavior
132+
1. Create a new XF: `POST /api/xf` with template body
133+
2. Verify it appears in the content listing
134+
3. Update the XF and confirm changes persist
135+
4. Delete and verify cascade behavior matches config
109136
110-
## Notes
137+
## 📝 Notes
111138
112-
- Tenants must configure `experienceFragmentComponentType` before using XF features
113-
- Performance testing recommended for sites with >1000 XFs
139+
- Configure `experienceFragmentComponentType` in tenant settings first
140+
- Performance testing recommended for sites with >1000 fragments
114141
```
115142
116-
## Emoji Placement
143+
## Emoji Usage
117144
118-
- When gitmoji enabled: ONE emoji in the H1 title, emojis in section headers for visual structure
119-
- Keep actual content clean — no scattered emojis in prose
120-
- When conventional preset: NO emojis anywhere
145+
Follow the emoji styling instructions injected by the system:
146+
- **When gitmoji enabled**: Use emojis in H1 title and section headers (🎯, ⚙️, 📋, ⚠️, 🧪, 📝). Include gitmoji from commits. Keep body text clean.
147+
- **When conventional/no-emoji preset**: No emojis anywhere — plain text only.
121148
"""

src/agents/iris.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -667,13 +667,19 @@ Guidelines:
667667
if gitmoji_enabled {
668668
system_prompt.push_str("\n\n=== EMOJI STYLING ===\n");
669669
system_prompt.push_str(
670-
"Include ONE relevant gitmoji at the start of the H1 title to indicate the primary type of change. ",
670+
"Use emojis to make the output visually scannable and engaging:\n",
671671
);
672672
system_prompt.push_str(
673-
"You may optionally add emojis to section headers (## headings) for visual structure. ",
673+
"- H1 title: ONE gitmoji at the start (✨, 🐛, ♻️, etc.)\n",
674674
);
675675
system_prompt.push_str(
676-
"Keep prose content clean - no scattered emojis within sentences or bullet points. ",
676+
"- Section headers (## headings): Add relevant emojis (🎯 What's New, ⚙️ How It Works, 📋 Commits, ⚠️ Breaking Changes, 🧪 Testing, 📝 Notes)\n",
677+
);
678+
system_prompt.push_str(
679+
"- Commit list entries: Include the gitmoji from each commit\n",
680+
);
681+
system_prompt.push_str(
682+
"- Body text: Keep clean - no scattered emojis within prose\n\n",
677683
);
678684
system_prompt.push_str("Choose from this gitmoji list:\n\n");
679685
system_prompt.push_str(&crate::gitmoji::get_gitmoji_list());

src/cli.rs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ pub enum Commands {
188188
#[arg(long, help = "Output raw markdown without any console formatting")]
189189
raw: bool,
190190

191+
/// Copy raw markdown to clipboard
192+
#[arg(
193+
short,
194+
long,
195+
help = "Copy raw markdown to clipboard (for pasting into GitHub/GitLab)"
196+
)]
197+
copy: bool,
198+
191199
/// Starting branch, commit, or commitish for comparison
192200
#[arg(
193201
long,
@@ -1127,9 +1135,10 @@ pub async fn handle_command(
11271135
common,
11281136
print,
11291137
raw,
1138+
copy,
11301139
from,
11311140
to,
1132-
} => handle_pr(common, print, raw, from, to, repository_url).await,
1141+
} => handle_pr(common, print, raw, copy, from, to, repository_url).await,
11331142
Commands::Studio {
11341143
common,
11351144
mode,
@@ -1234,14 +1243,16 @@ async fn handle_pr_with_agent(
12341243
common: CommonParams,
12351244
print: bool,
12361245
raw: bool,
1246+
copy: bool,
12371247
from: Option<String>,
12381248
to: Option<String>,
12391249
repository_url: Option<String>,
12401250
) -> anyhow::Result<()> {
1251+
use arboard::Clipboard;
12411252
use crate::agents::{IrisAgentService, StructuredResponse, TaskContext};
12421253
use crate::instruction_presets::PresetType;
12431254

1244-
// Check if the preset is appropriate for PR descriptions (skip for raw output)
1255+
// Check if the preset is appropriate for PR descriptions (skip for raw output only)
12451256
if !raw
12461257
&& !common.is_valid_preset_for_type(PresetType::Review)
12471258
&& !common.is_valid_preset_for_type(PresetType::Both)
@@ -1255,7 +1266,7 @@ async fn handle_pr_with_agent(
12551266
// Create structured context for PR (handles defaults: from=main, to=HEAD)
12561267
let context = TaskContext::for_pr(from, to);
12571268

1258-
// Create spinner for progress indication (skip for raw output)
1269+
// Create spinner for progress indication (skip for raw output only)
12591270
let spinner = if raw {
12601271
None
12611272
} else {
@@ -1276,7 +1287,31 @@ async fn handle_pr_with_agent(
12761287
return Err(anyhow::anyhow!("Expected pull request response"));
12771288
};
12781289

1279-
if raw || print {
1290+
// Handle clipboard copy
1291+
if copy {
1292+
let raw_content = generated_pr.raw_content();
1293+
match Clipboard::new() {
1294+
Ok(mut clipboard) => match clipboard.set_text(raw_content) {
1295+
Ok(()) => {
1296+
ui::print_success("PR description copied to clipboard");
1297+
}
1298+
Err(e) => {
1299+
ui::print_error(&format!("Failed to copy to clipboard: {e}"));
1300+
// Fall back to printing raw
1301+
println!("{raw_content}");
1302+
}
1303+
},
1304+
Err(e) => {
1305+
ui::print_error(&format!("Clipboard unavailable: {e}"));
1306+
// Fall back to printing raw
1307+
println!("{raw_content}");
1308+
}
1309+
}
1310+
} else if raw {
1311+
// Raw markdown for piping to files or APIs
1312+
println!("{}", generated_pr.raw_content());
1313+
} else if print {
1314+
// Formatted output for terminal viewing
12801315
println!("{}", generated_pr.format());
12811316
} else {
12821317
ui::print_success("PR description generated successfully");
@@ -1291,26 +1326,29 @@ async fn handle_pr(
12911326
common: CommonParams,
12921327
print: bool,
12931328
raw: bool,
1329+
copy: bool,
12941330
from: Option<String>,
12951331
to: Option<String>,
12961332
repository_url: Option<String>,
12971333
) -> anyhow::Result<()> {
12981334
log_debug!(
1299-
"Handling 'pr' command with common: {:?}, print: {}, raw: {}, from: {:?}, to: {:?}",
1335+
"Handling 'pr' command with common: {:?}, print: {}, raw: {}, copy: {}, from: {:?}, to: {:?}",
13001336
common,
13011337
print,
13021338
raw,
1339+
copy,
13031340
from,
13041341
to
13051342
);
13061343

1307-
// For raw output, skip all formatting
1344+
// For raw output, skip version banner (piped output should be clean)
1345+
// For copy mode, show the banner since we're giving user feedback
13081346
if !raw {
13091347
ui::print_version(crate_version!());
13101348
ui::print_newline();
13111349
}
13121350

1313-
handle_pr_with_agent(common, print, raw, from, to, repository_url).await
1351+
handle_pr_with_agent(common, print, raw, copy, from, to, repository_url).await
13141352
}
13151353

13161354
/// Handle the `Studio` command
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//! Commit count picker modal key handler
2+
//!
3+
//! Quick picker for "last N commits" - sets `from_ref` to HEAD~N
4+
5+
use crossterm::event::{KeyCode, KeyEvent};
6+
7+
use crate::studio::events::SideEffect;
8+
use crate::studio::state::{CommitCountTarget, Modal, Notification, StudioState};
9+
10+
use super::super::{
11+
reload_changelog_data, reload_pr_data, reload_release_notes_data, reload_review_data,
12+
};
13+
14+
/// Handle key events in commit count modal
15+
pub fn handle(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
16+
// Get current state
17+
let (input, target) = if let Some(Modal::CommitCount { input, target }) = &state.modal {
18+
(input.clone(), *target)
19+
} else {
20+
return vec![];
21+
};
22+
23+
match key.code {
24+
KeyCode::Esc => {
25+
state.close_modal();
26+
vec![]
27+
}
28+
KeyCode::Enter => {
29+
// Parse the number and set HEAD~N
30+
let count: usize = input.parse().unwrap_or(0);
31+
if count == 0 {
32+
state.notify(Notification::error("Please enter a number > 0"));
33+
return vec![];
34+
}
35+
36+
let ref_value = format!("HEAD~{count}");
37+
let (label, effect) = match target {
38+
CommitCountTarget::Pr => {
39+
state.modes.pr.base_branch.clone_from(&ref_value);
40+
("PR base", reload_pr_data(state))
41+
}
42+
CommitCountTarget::Review => {
43+
state.modes.review.from_ref.clone_from(&ref_value);
44+
("Review from", reload_review_data(state))
45+
}
46+
CommitCountTarget::Changelog => {
47+
state.modes.changelog.from_ref.clone_from(&ref_value);
48+
("Changelog from", reload_changelog_data(state))
49+
}
50+
CommitCountTarget::ReleaseNotes => {
51+
state.modes.release_notes.from_ref.clone_from(&ref_value);
52+
("Release Notes from", reload_release_notes_data(state))
53+
}
54+
};
55+
56+
state.notify(Notification::info(format!(
57+
"{label} set to last {count} commits"
58+
)));
59+
state.close_modal();
60+
vec![effect]
61+
}
62+
// Quick presets: 1-9 for immediate selection
63+
KeyCode::Char(c) if c.is_ascii_digit() => {
64+
if let Some(Modal::CommitCount { input, .. }) = &mut state.modal {
65+
input.push(c);
66+
}
67+
state.mark_dirty();
68+
vec![]
69+
}
70+
KeyCode::Backspace => {
71+
if let Some(Modal::CommitCount { input, .. }) = &mut state.modal {
72+
input.pop();
73+
}
74+
state.mark_dirty();
75+
vec![]
76+
}
77+
_ => vec![],
78+
}
79+
}

src/studio/handlers/modals/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Each modal type has its own handler module for maintainability.
44
55
mod chat;
6+
mod commit_count;
67
mod confirm;
78
mod emoji_selector;
89
mod instructions;
@@ -34,6 +35,7 @@ pub fn handle_modal_key(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffec
3435
Some(Modal::EmojiSelector { .. }) => emoji_selector::handle(state, key),
3536
Some(Modal::Settings(_)) => settings::handle(state, key),
3637
Some(Modal::ThemeSelector { .. }) => theme_selector::handle(state, key),
38+
Some(Modal::CommitCount { .. }) => commit_count::handle(state, key),
3739
None => vec![],
3840
}
3941
}

0 commit comments

Comments
 (0)