Skip to content

Commit c87731d

Browse files
committed
✨ Add amend mode to Iris Studio for modifying previous commits
Implement comprehensive support for amending commits directly within Studio: - Add amend_mode state with original message tracking in CommitState - Introduce ToggleAmendMode event and Shift+A keybinding to toggle mode - Create ConfirmAmend modal and ExecuteAmend side effect for the workflow - Update commit generation to use TaskContext::for_amend() with original message context when amending - Add perform_amend() method and ExitResult::Amended variant in app - Add clear() method to MessageEditorState for mode switching - Display "[AMEND]" indicator in commit panel title when mode is active Users can now press Shift+A to toggle amend mode, which regenerates the commit message considering the original commit, then amends HEAD with the new message and any staged changes.
1 parent 7e34b94 commit c87731d

File tree

13 files changed

+190
-16
lines changed

13 files changed

+190
-16
lines changed

src/cli.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -572,9 +572,7 @@ async fn handle_gen_with_agent(
572572
// Use IrisAgentService for commit message generation
573573
// For amend, we pass the original message as context
574574
let context = if config.amend {
575-
let original_message = commit_service
576-
.get_head_commit_message()
577-
.unwrap_or_default();
575+
let original_message = commit_service.get_head_commit_message().unwrap_or_default();
578576
TaskContext::for_amend(original_message)
579577
} else {
580578
TaskContext::for_gen()

src/git/commit.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ pub fn amend_commit(repo: &Repository, message: &str, is_remote: bool) -> Result
134134

135135
// Amend the HEAD commit with the new tree and message
136136
let commit_oid = head_commit.amend(
137-
Some("HEAD"), // Update the HEAD reference
138-
Some(&signature), // New author (use current)
139-
Some(&signature), // New committer (use current)
140-
None, // Keep original encoding
141-
Some(message), // New message
142-
Some(&tree), // New tree (includes staged changes)
137+
Some("HEAD"), // Update the HEAD reference
138+
Some(&signature), // New author (use current)
139+
Some(&signature), // New committer (use current)
140+
None, // Keep original encoding
141+
Some(message), // New message
142+
Some(&tree), // New tree (includes staged changes)
143143
)?;
144144

145145
let branch_name = repo.head()?.shorthand().unwrap_or("HEAD").to_string();

src/studio/app/agent_tasks.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
541541
instructions: Option<String>,
542542
preset: String,
543543
use_gitmoji: bool,
544+
amend: bool,
544545
) {
545546
use crate::agents::{StructuredResponse, TaskContext};
546547

@@ -553,11 +554,26 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
553554
return;
554555
};
555556

557+
// Get original message for amend mode
558+
let original_message = if amend {
559+
self.state
560+
.repo
561+
.as_ref()
562+
.and_then(|r| r.get_head_commit_message().ok())
563+
.unwrap_or_default()
564+
} else {
565+
String::new()
566+
};
567+
556568
let tx = self.iris_result_tx.clone();
557569

558570
tokio::spawn(async move {
559-
// Use standard commit context
560-
let context = TaskContext::for_gen();
571+
// Use amend context if amending, otherwise standard commit context
572+
let context = if amend {
573+
TaskContext::for_amend(original_message)
574+
} else {
575+
TaskContext::for_gen()
576+
};
561577

562578
// Execute commit capability with style overrides
563579
let preset_opt = if preset == "default" {

src/studio/app/mod.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ impl StudioApp {
195195
return Some(self.perform_commit(&message));
196196
}
197197

198+
SideEffect::ExecuteAmend { message } => {
199+
return Some(self.perform_amend(&message));
200+
}
201+
198202
SideEffect::Redraw => {
199203
self.state.mark_dirty();
200204
}
@@ -262,8 +266,9 @@ impl StudioApp {
262266
instructions,
263267
preset,
264268
use_gitmoji,
269+
amend,
265270
} => {
266-
self.spawn_commit_generation(instructions, preset, use_gitmoji);
271+
self.spawn_commit_generation(instructions, preset, use_gitmoji, amend);
267272
}
268273
AgentTask::Review { from_ref, to_ref } => {
269274
self.spawn_review_generation(from_ref, to_ref);
@@ -751,7 +756,8 @@ impl StudioApp {
751756
self.state.modes.commit.generating = true;
752757
let preset = self.state.modes.commit.preset.clone();
753758
let use_gitmoji = self.state.modes.commit.use_gitmoji;
754-
self.spawn_commit_generation(None, preset, use_gitmoji);
759+
let amend = self.state.modes.commit.amend_mode;
760+
self.spawn_commit_generation(None, preset, use_gitmoji, amend);
755761
}
756762

757763
/// Auto-generate code review on mode entry
@@ -1059,6 +1065,20 @@ impl StudioApp {
10591065
}
10601066
}
10611067

1068+
fn perform_amend(&self, message: &str) -> ExitResult {
1069+
if let Some(service) = &self.commit_service {
1070+
match service.perform_amend(message) {
1071+
Ok(result) => {
1072+
let output = crate::output::format_commit_result(&result, message);
1073+
ExitResult::Amended(output)
1074+
}
1075+
Err(e) => ExitResult::Error(e.to_string()),
1076+
}
1077+
} else {
1078+
ExitResult::Error("Commit service not available".to_string())
1079+
}
1080+
}
1081+
10621082
// ═══════════════════════════════════════════════════════════════════════════
10631083
// Rendering
10641084
// ═══════════════════════════════════════════════════════════════════════════
@@ -1758,6 +1778,8 @@ pub enum ExitResult {
17581778
Quit,
17591779
/// User committed changes (with output message)
17601780
Committed(String),
1781+
/// User amended the previous commit (with output message)
1782+
Amended(String),
17611783
/// An error occurred
17621784
Error(String),
17631785
}
@@ -1818,6 +1840,10 @@ pub fn run_studio(
18181840
println!("{message}");
18191841
Ok(())
18201842
}
1843+
ExitResult::Amended(message) => {
1844+
println!("{message}");
1845+
Ok(())
1846+
}
18211847
ExitResult::Error(error) => Err(anyhow!("{}", error)),
18221848
}
18231849
}

src/studio/components/message_editor.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,19 @@ impl MessageEditorState {
142142
self.edit_mode = false;
143143
}
144144

145+
/// Clear all messages and reset state
146+
pub fn clear(&mut self) {
147+
self.generated_messages.clear();
148+
self.selected_message = 0;
149+
self.original_message.clear();
150+
self.textarea = TextArea::default();
151+
self.textarea
152+
.set_cursor_line_style(Style::default().bg(theme::bg_highlight_color()));
153+
self.textarea
154+
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
155+
self.edit_mode = false;
156+
}
157+
145158
/// Get current message text
146159
pub fn get_message(&self) -> String {
147160
self.textarea.lines().join("\n")

src/studio/events.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@ pub enum StudioEvent {
5656
instructions: Option<String>,
5757
preset: String,
5858
use_gitmoji: bool,
59+
amend: bool,
5960
},
6061

62+
/// Toggle amend mode for commit
63+
ToggleAmendMode,
64+
6165
/// Generate code review
6266
GenerateReview { from_ref: String, to_ref: String },
6367

@@ -337,6 +341,7 @@ pub enum ModalType {
337341
EmojiSelector,
338342
RefSelector { field: RefField },
339343
ConfirmCommit,
344+
ConfirmAmend,
340345
ConfirmQuit,
341346
}
342347

@@ -412,6 +417,9 @@ pub enum SideEffect {
412417
/// Execute git commit
413418
ExecuteCommit { message: String },
414419

420+
/// Execute git commit --amend
421+
ExecuteAmend { message: String },
422+
415423
/// Show notification (if needs timing/animation)
416424
#[allow(dead_code)] // Kept for future use - handled in executor but not yet constructed
417425
ShowNotification {
@@ -463,6 +471,7 @@ pub enum AgentTask {
463471
instructions: Option<String>,
464472
preset: String,
465473
use_gitmoji: bool,
474+
amend: bool,
466475
},
467476
Review {
468477
from_ref: String,

src/studio/handlers/commit.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,39 @@ fn handle_message_key(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect>
286286
vec![]
287287
}
288288

289-
// Commit - use message from editor (may have been modified)
289+
// Toggle amend mode
290+
KeyCode::Char('A') => {
291+
state.modes.commit.amend_mode = !state.modes.commit.amend_mode;
292+
if state.modes.commit.amend_mode {
293+
// Load original message from HEAD
294+
if let Some(repo) = &state.repo
295+
&& let Ok(msg) = repo.get_head_commit_message()
296+
{
297+
state.modes.commit.original_message = Some(msg);
298+
}
299+
state.notify(crate::studio::state::Notification::info(
300+
"Amend mode: ON - will replace previous commit".to_string(),
301+
));
302+
} else {
303+
state.modes.commit.original_message = None;
304+
state.notify(crate::studio::state::Notification::info(
305+
"Amend mode: OFF".to_string(),
306+
));
307+
}
308+
// Clear messages when toggling amend mode (they need regeneration)
309+
state.modes.commit.messages.clear();
310+
state.modes.commit.message_editor.clear();
311+
state.mark_dirty();
312+
vec![]
313+
}
314+
315+
// Commit/Amend - use message from editor (may have been modified)
290316
KeyCode::Enter => {
291317
let message = state.modes.commit.message_editor.get_message();
292318
if message.is_empty() {
293319
vec![]
320+
} else if state.modes.commit.amend_mode {
321+
vec![SideEffect::ExecuteAmend { message }]
294322
} else {
295323
vec![SideEffect::ExecuteCommit { message }]
296324
}

src/studio/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ pub fn spawn_commit_task(state: &StudioState) -> SideEffect {
268268
},
269269
preset: state.modes.commit.preset.clone(),
270270
use_gitmoji: state.modes.commit.emoji_mode != EmojiMode::None,
271+
amend: state.modes.commit.amend_mode,
271272
},
272273
}
273274
}

src/studio/handlers/modals/confirm.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ pub fn handle(state: &mut StudioState, key: KeyEvent) -> Vec<SideEffect> {
3434
vec![]
3535
}
3636
}
37+
"amend" => {
38+
// Get the commit message and execute amend
39+
if let Some(msg) = state
40+
.modes
41+
.commit
42+
.messages
43+
.get(state.modes.commit.current_index)
44+
{
45+
let message = crate::types::format_commit_message(msg);
46+
vec![SideEffect::ExecuteAmend { message }]
47+
} else {
48+
vec![]
49+
}
50+
}
3751
"quit" => vec![SideEffect::Quit],
3852
_ => vec![],
3953
}

src/studio/reducer/mod.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,23 @@ pub fn reduce(
133133
instructions,
134134
preset,
135135
use_gitmoji,
136+
amend,
136137
} => {
137138
state.modes.commit.generating = true;
138-
state.set_iris_thinking("Generating commit message...");
139+
let thinking_msg = if amend {
140+
"Generating amended commit message..."
141+
} else {
142+
"Generating commit message..."
143+
};
144+
state.set_iris_thinking(thinking_msg);
139145
history.record_agent_start(TaskType::Commit);
140146

141147
effects.push(SideEffect::SpawnAgent {
142148
task: AgentTask::Commit {
143149
instructions,
144150
preset,
145151
use_gitmoji,
152+
amend,
146153
},
147154
});
148155
}
@@ -610,6 +617,17 @@ pub fn reduce(
610617
effects.push(SideEffect::ExecuteCommit { message });
611618
}
612619
}
620+
ModalType::ConfirmAmend => {
621+
if let Some(msg) = state
622+
.modes
623+
.commit
624+
.messages
625+
.get(state.modes.commit.current_index)
626+
{
627+
let message = crate::types::format_commit_message(msg);
628+
effects.push(SideEffect::ExecuteAmend { message });
629+
}
630+
}
613631
ModalType::RefSelector { field } => {
614632
if let Some(ref_value) = data {
615633
modal::apply_ref_selection(state, field, ref_value);
@@ -723,6 +741,24 @@ pub fn reduce(
723741
state.mark_dirty();
724742
}
725743

744+
StudioEvent::ToggleAmendMode => {
745+
state.modes.commit.amend_mode = !state.modes.commit.amend_mode;
746+
if state.modes.commit.amend_mode {
747+
// Load original message from HEAD if repo is available
748+
if let Some(repo) = &state.repo
749+
&& let Ok(msg) = repo.get_head_commit_message()
750+
{
751+
state.modes.commit.original_message = Some(msg);
752+
}
753+
} else {
754+
state.modes.commit.original_message = None;
755+
}
756+
// Clear messages when toggling amend mode
757+
state.modes.commit.messages.clear();
758+
state.modes.commit.message_editor.clear();
759+
state.mark_dirty();
760+
}
761+
726762
// ─────────────────────────────────────────────────────────────────────────
727763
// Lifecycle Events
728764
// ─────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)