Skip to content

Commit 7f59388

Browse files
committed
♻️ Obliterate state duplication with single source of truth
Rip out the redundant ChatState cloning that plagued Modal::Chat! The old approach was a maintenance nightmare: chat state lived in two places at once, demanding constant sync gymnastics on open/close and scattered update logic across handlers and reducers. No more. Now Modal::Chat is a lean unit variant—just a signal that says "chat is open." All state mutations flow directly to StudioState.chat_state, the one true source. Clean. Simple. Bulletproof. Bonus wins: - IrisTaskResult::Error now carries task_type for precise error routing (no more guessing games with generating flags) - Chat handler streamlined to hit state.chat_state directly - Reducer.rs purged of redundant modal pattern matching cruft
1 parent ee86f6c commit 7f59388

File tree

6 files changed

+121
-165
lines changed

6 files changed

+121
-165
lines changed

src/studio/app.rs

Lines changed: 88 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ pub enum IrisTaskResult {
7676
StreamingComplete { task_type: TaskType },
7777
/// Semantic blame result
7878
SemanticBlame(SemanticBlameResult),
79-
/// Error from the task
80-
Error(String),
79+
/// Error from the task (includes which task failed)
80+
Error { task_type: TaskType, error: String },
8181
}
8282

8383
/// Type of content update triggered by chat
@@ -648,8 +648,6 @@ impl StudioApp {
648648
/// Check for completed Iris task results
649649
/// Convert async Iris results to events and push to queue
650650
fn check_iris_results(&mut self) {
651-
use super::state::Modal;
652-
653651
while let Ok(result) = self.iris_result_rx.try_recv() {
654652
let event = match result {
655653
IrisTaskResult::CommitMessages(messages) => StudioEvent::AgentComplete {
@@ -709,13 +707,6 @@ impl StudioApp {
709707
IrisTaskResult::ToolStatus { tool_name, message } => {
710708
// Tool status updates - move current tool to history, set new current
711709
let tool_desc = format!("{} - {}", tool_name, message);
712-
if let Some(Modal::Chat(chat)) = &mut self.state.modal {
713-
// Move previous tool to history (bounded)
714-
if let Some(prev) = chat.current_tool.take() {
715-
chat.add_tool_to_history(prev);
716-
}
717-
chat.current_tool = Some(tool_desc.clone());
718-
}
719710
if let Some(prev) = self.state.chat_state.current_tool.take() {
720711
self.state.chat_state.add_tool_to_history(prev);
721712
}
@@ -738,31 +729,8 @@ impl StudioApp {
738729
StudioEvent::StreamingComplete { task_type }
739730
}
740731

741-
IrisTaskResult::Error(err) => {
742-
// Determine which task failed based on what's currently generating
743-
let task_type = if self.state.modes.commit.generating {
744-
TaskType::Commit
745-
} else if self.state.modes.review.generating {
746-
TaskType::Review
747-
} else if self.state.modes.pr.generating {
748-
TaskType::PR
749-
} else if self.state.modes.changelog.generating {
750-
TaskType::Changelog
751-
} else if self.state.modes.release_notes.generating {
752-
TaskType::ReleaseNotes
753-
} else if matches!(&self.state.modal, Some(Modal::Chat(c)) if c.is_responding) {
754-
TaskType::Chat
755-
} else if self.state.modes.explore.blame_loading {
756-
TaskType::SemanticBlame
757-
} else {
758-
// Default to commit if we can't determine
759-
TaskType::Commit
760-
};
761-
762-
StudioEvent::AgentError {
763-
task_type,
764-
error: err,
765-
}
732+
IrisTaskResult::Error { task_type, error } => {
733+
StudioEvent::AgentError { task_type, error }
766734
}
767735
};
768736

@@ -774,7 +742,7 @@ impl StudioApp {
774742
use crate::agents::StructuredResponse;
775743
use crate::agents::status::IRIS_STATUS;
776744
use crate::agents::tools::{ContentUpdate, create_content_update_channel};
777-
use crate::studio::state::{ChatMessage, ChatRole, Modal};
745+
use crate::studio::state::{ChatMessage, ChatRole};
778746
use std::sync::Arc;
779747
use std::sync::atomic::{AtomicBool, Ordering};
780748

@@ -795,12 +763,9 @@ impl StudioApp {
795763
let tx_updates = self.iris_result_tx.clone();
796764
let mode = context.mode;
797765

798-
// Extract conversation history from chat modal (convert VecDeque → Vec)
799-
let chat_history: Vec<ChatMessage> = if let Some(Modal::Chat(chat)) = &self.state.modal {
800-
chat.messages.iter().cloned().collect()
801-
} else {
802-
Vec::new()
803-
};
766+
// Extract conversation history (convert VecDeque → Vec)
767+
let chat_history: Vec<ChatMessage> =
768+
self.state.chat_state.messages.iter().cloned().collect();
804769

805770
// Use context content if provided, otherwise extract from state
806771
let current_content = context
@@ -1036,9 +1001,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
10361001

10371002
let Some(agent) = self.agent_service.clone() else {
10381003
let tx = self.iris_result_tx.clone();
1039-
let _ = tx.send(IrisTaskResult::Error(
1040-
"Agent service not available".to_string(),
1041-
));
1004+
let _ = tx.send(IrisTaskResult::Error {
1005+
task_type: TaskType::Review,
1006+
error: "Agent service not available".to_string(),
1007+
});
10421008
return;
10431009
};
10441010

@@ -1050,7 +1016,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
10501016
let context = match TaskContext::for_review(None, Some(from_ref), Some(to_ref), false) {
10511017
Ok(ctx) => ctx,
10521018
Err(e) => {
1053-
let _ = tx.send(IrisTaskResult::Error(format!("Context error: {}", e)));
1019+
let _ = tx.send(IrisTaskResult::Error {
1020+
task_type: TaskType::Review,
1021+
error: format!("Context error: {}", e),
1022+
});
10541023
return;
10551024
}
10561025
};
@@ -1086,7 +1055,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
10861055
let _ = tx.send(IrisTaskResult::ReviewContent(review_text));
10871056
}
10881057
Err(e) => {
1089-
let _ = tx.send(IrisTaskResult::Error(format!("Review error: {}", e)));
1058+
let _ = tx.send(IrisTaskResult::Error {
1059+
task_type: TaskType::Review,
1060+
error: format!("Review error: {}", e),
1061+
});
10901062
}
10911063
}
10921064
});
@@ -1098,9 +1070,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
10981070

10991071
let Some(agent) = self.agent_service.clone() else {
11001072
let tx = self.iris_result_tx.clone();
1101-
let _ = tx.send(IrisTaskResult::Error(
1102-
"Agent service not available".to_string(),
1103-
));
1073+
let _ = tx.send(IrisTaskResult::Error {
1074+
task_type: TaskType::PR,
1075+
error: "Agent service not available".to_string(),
1076+
});
11041077
return;
11051078
};
11061079

@@ -1137,7 +1110,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
11371110
let _ = tx.send(IrisTaskResult::PRContent(pr_text));
11381111
}
11391112
Err(e) => {
1140-
let _ = tx.send(IrisTaskResult::Error(format!("PR error: {}", e)));
1113+
let _ = tx.send(IrisTaskResult::Error {
1114+
task_type: TaskType::PR,
1115+
error: format!("PR error: {}", e),
1116+
});
11411117
}
11421118
}
11431119
});
@@ -1149,9 +1125,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
11491125

11501126
let Some(agent) = self.agent_service.clone() else {
11511127
let tx = self.iris_result_tx.clone();
1152-
let _ = tx.send(IrisTaskResult::Error(
1153-
"Agent service not available".to_string(),
1154-
));
1128+
let _ = tx.send(IrisTaskResult::Error {
1129+
task_type: TaskType::Changelog,
1130+
error: "Agent service not available".to_string(),
1131+
});
11551132
return;
11561133
};
11571134

@@ -1191,7 +1168,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
11911168
let _ = tx.send(IrisTaskResult::ChangelogContent(changelog_text));
11921169
}
11931170
Err(e) => {
1194-
let _ = tx.send(IrisTaskResult::Error(format!("Changelog error: {}", e)));
1171+
let _ = tx.send(IrisTaskResult::Error {
1172+
task_type: TaskType::Changelog,
1173+
error: format!("Changelog error: {}", e),
1174+
});
11951175
}
11961176
}
11971177
});
@@ -1203,9 +1183,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
12031183

12041184
let Some(agent) = self.agent_service.clone() else {
12051185
let tx = self.iris_result_tx.clone();
1206-
let _ = tx.send(IrisTaskResult::Error(
1207-
"Agent service not available".to_string(),
1208-
));
1186+
let _ = tx.send(IrisTaskResult::Error {
1187+
task_type: TaskType::ReleaseNotes,
1188+
error: "Agent service not available".to_string(),
1189+
});
12091190
return;
12101191
};
12111192

@@ -1245,7 +1226,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
12451226
let _ = tx.send(IrisTaskResult::ReleaseNotesContent(release_notes_text));
12461227
}
12471228
Err(e) => {
1248-
let _ = tx.send(IrisTaskResult::Error(format!("Release notes error: {}", e)));
1229+
let _ = tx.send(IrisTaskResult::Error {
1230+
task_type: TaskType::ReleaseNotes,
1231+
error: format!("Release notes error: {}", e),
1232+
});
12491233
}
12501234
}
12511235
});
@@ -1567,9 +1551,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
15671551

15681552
let Some(agent) = self.agent_service.clone() else {
15691553
let tx = self.iris_result_tx.clone();
1570-
let _ = tx.send(IrisTaskResult::Error(
1571-
"Agent service not available".to_string(),
1572-
));
1554+
let _ = tx.send(IrisTaskResult::Error {
1555+
task_type: TaskType::Commit,
1556+
error: "Agent service not available".to_string(),
1557+
});
15731558
return;
15741559
};
15751560

@@ -1597,14 +1582,18 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
15971582
let _ = tx.send(IrisTaskResult::CommitMessages(vec![msg]));
15981583
}
15991584
_ => {
1600-
let _ = tx.send(IrisTaskResult::Error(
1601-
"Unexpected response type from agent".to_string(),
1602-
));
1585+
let _ = tx.send(IrisTaskResult::Error {
1586+
task_type: TaskType::Commit,
1587+
error: "Unexpected response type from agent".to_string(),
1588+
});
16031589
}
16041590
}
16051591
}
16061592
Err(e) => {
1607-
let _ = tx.send(IrisTaskResult::Error(format!("Agent error: {}", e)));
1593+
let _ = tx.send(IrisTaskResult::Error {
1594+
task_type: TaskType::Commit,
1595+
error: format!("Agent error: {}", e),
1596+
});
16081597
}
16091598
}
16101599
});
@@ -1617,9 +1606,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
16171606

16181607
let Some(repo) = &self.state.repo else {
16191608
let tx = self.iris_result_tx.clone();
1620-
let _ = tx.send(IrisTaskResult::Error(
1621-
"Repository not available".to_string(),
1622-
));
1609+
let _ = tx.send(IrisTaskResult::Error {
1610+
task_type: TaskType::SemanticBlame,
1611+
error: "Repository not available".to_string(),
1612+
});
16231613
return;
16241614
};
16251615

@@ -1629,15 +1619,21 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
16291619
let lines: Vec<&str> = content.lines().collect();
16301620
if start_line == 0 || start_line > lines.len() {
16311621
let tx = self.iris_result_tx.clone();
1632-
let _ = tx.send(IrisTaskResult::Error("Invalid line range".to_string()));
1622+
let _ = tx.send(IrisTaskResult::Error {
1623+
task_type: TaskType::SemanticBlame,
1624+
error: "Invalid line range".to_string(),
1625+
});
16331626
return;
16341627
}
16351628
let end = end_line.min(lines.len());
16361629
lines[(start_line - 1)..end].join("\n")
16371630
}
16381631
Err(e) => {
16391632
let tx = self.iris_result_tx.clone();
1640-
let _ = tx.send(IrisTaskResult::Error(format!("Could not read file: {}", e)));
1633+
let _ = tx.send(IrisTaskResult::Error {
1634+
task_type: TaskType::SemanticBlame,
1635+
error: format!("Could not read file: {}", e),
1636+
});
16411637
return;
16421638
}
16431639
};
@@ -1666,12 +1662,18 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
16661662
Ok(output) => {
16671663
let err = String::from_utf8_lossy(&output.stderr);
16681664
let tx = self.iris_result_tx.clone();
1669-
let _ = tx.send(IrisTaskResult::Error(format!("Git blame failed: {}", err)));
1665+
let _ = tx.send(IrisTaskResult::Error {
1666+
task_type: TaskType::SemanticBlame,
1667+
error: format!("Git blame failed: {}", err),
1668+
});
16701669
return;
16711670
}
16721671
Err(e) => {
16731672
let tx = self.iris_result_tx.clone();
1674-
let _ = tx.send(IrisTaskResult::Error(format!("Could not run git: {}", e)));
1673+
let _ = tx.send(IrisTaskResult::Error {
1674+
task_type: TaskType::SemanticBlame,
1675+
error: format!("Could not run git: {}", e),
1676+
});
16751677
return;
16761678
}
16771679
};
@@ -1697,9 +1699,10 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
16971699

16981700
let Some(agent) = self.agent_service.clone() else {
16991701
let tx = self.iris_result_tx.clone();
1700-
let _ = tx.send(IrisTaskResult::Error(
1701-
"Agent service not available".to_string(),
1702-
));
1702+
let _ = tx.send(IrisTaskResult::Error {
1703+
task_type: TaskType::SemanticBlame,
1704+
error: "Agent service not available".to_string(),
1705+
});
17031706
return;
17041707
};
17051708

@@ -1739,16 +1742,17 @@ Simply call the appropriate tool with the new content. Do NOT echo back the full
17391742
let _ = tx.send(IrisTaskResult::SemanticBlame(result));
17401743
}
17411744
_ => {
1742-
let _ = tx.send(IrisTaskResult::Error(
1743-
"Unexpected response type from agent".to_string(),
1744-
));
1745+
let _ = tx.send(IrisTaskResult::Error {
1746+
task_type: TaskType::SemanticBlame,
1747+
error: "Unexpected response type from agent".to_string(),
1748+
});
17451749
}
17461750
},
17471751
Err(e) => {
1748-
let _ = tx.send(IrisTaskResult::Error(format!(
1749-
"Semantic blame error: {}",
1750-
e
1751-
)));
1752+
let _ = tx.send(IrisTaskResult::Error {
1753+
task_type: TaskType::SemanticBlame,
1754+
error: format!("Semantic blame error: {}", e),
1755+
});
17521756
}
17531757
}
17541758
});

0 commit comments

Comments
 (0)