pub mod types;
pub use types::{AppMode, FocusedPane, InputMode, MergeQueue, MergeTask, Notification, ReviewFocus, ReviewState};
use crate::agent::{AgentManager, AgentStatus};
use crate::git_utils::{detect_github_repo, get_commit_log, get_worker_commits};
use anyhow::{Context, Result};
use cctakt::{
available_themes, create_theme, current_theme_id, debug, render_task, set_theme,
Config, DiffView, GitHubClient, Issue, IssuePicker, MergeManager, Plan, PlanManager,
suggest_branch_name, TaskAction, TaskResult, TaskStatus, WorktreeManager,
};
use std::env;
use std::path::PathBuf;
use std::process::Command;
pub struct App {
pub agent_manager: AgentManager,
pub should_quit: bool,
pub content_rows: u16,
pub content_cols: u16,
pub mode: AppMode,
pub focused_pane: FocusedPane,
pub input_mode: InputMode,
pub config: Config,
pub worktree_manager: Option<WorktreeManager>,
pub github_client: Option<GitHubClient>,
pub issue_picker: IssuePicker,
pub agent_issues: Vec<Option<Issue>>,
pub agent_worktrees: Vec<Option<PathBuf>>,
pub review_state: Option<ReviewState>,
pub plan_manager: PlanManager,
pub current_plan: Option<Plan>,
pub task_agents: std::collections::HashMap<String, usize>,
pub notifications: Vec<Notification>,
pub pending_agent_prompt: Option<String>,
pub prompt_delay_frames: u32,
pub pending_review_task_id: Option<String>,
pub merge_queue: MergeQueue,
pub show_theme_picker: bool,
pub theme_picker_index: usize,
pub build_worker_index: Option<usize>,
pub build_worker_branch: Option<String>,
pub command_buffer: String,
}
impl App {
pub fn new(rows: u16, cols: u16, config: Config) -> Self {
let worktree_manager = WorktreeManager::from_current_dir().ok();
let github_client = config
.github
.repository
.as_ref()
.and_then(|repo| GitHubClient::new(repo).ok());
Self {
agent_manager: AgentManager::new(),
should_quit: false,
content_rows: rows,
content_cols: cols,
mode: AppMode::Normal,
focused_pane: FocusedPane::Right, input_mode: InputMode::Input, config,
worktree_manager,
github_client,
issue_picker: IssuePicker::new(),
agent_issues: Vec::new(),
agent_worktrees: Vec::new(),
review_state: None,
plan_manager: PlanManager::current_dir(),
current_plan: None,
task_agents: std::collections::HashMap::new(),
notifications: Vec::new(),
pending_agent_prompt: None,
prompt_delay_frames: 0,
pending_review_task_id: None,
merge_queue: MergeQueue::new(),
show_theme_picker: false,
theme_picker_index: 0,
build_worker_index: None,
build_worker_branch: None,
command_buffer: String::new(),
}
}
pub fn open_issue_picker(&mut self) {
if self.github_client.is_none() {
if let Some(repo) = detect_github_repo() {
self.github_client = GitHubClient::new(&repo).ok();
}
}
if self.github_client.is_some() {
self.mode = AppMode::IssuePicker;
self.add_notification(
"Opening issue picker...".to_string(),
cctakt::plan::NotifyLevel::Info,
);
self.fetch_issues();
} else {
self.add_notification(
"GitHub repository not configured. Set 'repository' in cctakt.toml or add a git remote.".to_string(),
cctakt::plan::NotifyLevel::Warning,
);
}
}
pub fn open_theme_picker(&mut self) {
let current = current_theme_id().id();
let themes = available_themes();
self.theme_picker_index = themes
.iter()
.position(|(id, _, _)| *id == current)
.unwrap_or(0);
self.show_theme_picker = true;
self.mode = AppMode::ThemePicker;
}
pub fn apply_theme(&mut self, theme_id: &str) {
set_theme(create_theme(theme_id));
self.config.theme = theme_id.to_string();
if let Err(e) = self.config.save() {
self.add_notification(
format!("Failed to save theme: {e}"),
cctakt::plan::NotifyLevel::Warning,
);
} else {
let themes = available_themes();
let name = themes
.iter()
.find(|(id, _, _)| *id == theme_id)
.map(|(_, name, _)| *name)
.unwrap_or(theme_id);
self.add_notification(
format!("Theme changed to {name}"),
cctakt::plan::NotifyLevel::Success,
);
}
}
pub fn fetch_issues(&mut self) {
self.issue_picker.set_loading(true);
if let Some(ref client) = self.github_client {
let labels: Vec<&str> = self
.config
.github
.labels
.iter()
.map(|s| s.as_str())
.collect();
match client.fetch_issues(&labels, "open") {
Ok(issues) => {
let count = issues.len();
self.issue_picker.set_issues(issues);
self.issue_picker.set_loading(false);
if count == 0 {
self.add_notification(
"No open issues found in repository.".to_string(),
cctakt::plan::NotifyLevel::Info,
);
}
}
Err(e) => {
self.issue_picker.set_error(Some(e.to_string()));
self.add_notification(
format!("Failed to fetch issues: {e}"),
cctakt::plan::NotifyLevel::Error,
);
}
}
}
}
pub fn add_agent_from_issue(&mut self, issue: Issue) -> Result<()> {
let branch_name = suggest_branch_name(&issue, &self.config.branch_prefix);
let (working_dir, worktree_path) = if let Some(ref wt_manager) = self.worktree_manager {
match wt_manager.create(&branch_name, &self.config.worktree_dir) {
Ok(path) => (path.clone(), Some(path)),
Err(_) => (
env::current_dir().context("Failed to get current directory")?,
None,
),
}
} else {
(
env::current_dir().context("Failed to get current directory")?,
None,
)
};
let task_prompt = render_task(&issue);
let name = format!("#{}", issue.number);
self.agent_manager
.add_non_interactive(name, working_dir, &task_prompt, None, Some(branch_name))?;
self.agent_issues.push(Some(issue));
self.agent_worktrees.push(worktree_path);
self.update_agent_sizes();
Ok(())
}
pub fn add_agent(&mut self) -> Result<()> {
let working_dir = env::current_dir().context("Failed to get current directory")?;
let name = working_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unnamed")
.to_string();
let agent_count = self.agent_manager.list().len();
let display_name = if agent_count == 0 {
name
} else {
format!("{}-{}", name, agent_count + 1)
};
self.agent_manager
.add(display_name, working_dir, self.content_rows, self.content_cols)?;
self.agent_issues.push(None);
self.agent_worktrees.push(None);
Ok(())
}
pub fn close_active_agent(&mut self) {
let index = self.agent_manager.active_index();
self.agent_manager.close(index);
if index < self.agent_issues.len() {
self.agent_issues.remove(index);
}
if index < self.agent_worktrees.len() {
self.agent_worktrees.remove(index);
}
self.update_agent_sizes();
}
pub fn check_agent_completion(&mut self) {
use std::time::Duration;
if self.mode == AppMode::ReviewMerge {
return;
}
let idle_threshold = Duration::from_secs(5);
let mut completed_agent: Option<(usize, String)> = None;
for i in 0..self.agent_manager.list().len() {
if let Some(agent) = self.agent_manager.get_mut(i) {
if agent.update_work_state(idle_threshold) {
completed_agent = Some((i, agent.name.clone()));
break;
}
}
}
if let Some((index, name)) = completed_agent {
self.add_notification(
format!("Agent '{name}' completed work. Starting review..."),
cctakt::plan::NotifyLevel::Success,
);
self.agent_manager.switch_to(index);
self.start_review(index);
}
}
pub fn start_review(&mut self, agent_index: usize) {
let worktree_path = if agent_index < self.agent_worktrees.len() {
self.agent_worktrees[agent_index].clone()
} else {
None
};
let Some(worktree_path) = worktree_path else {
return;
};
let branch = Command::new("git")
.current_dir(&worktree_path)
.args(["branch", "--show-current"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
let repo_path = env::current_dir().unwrap_or_default();
let merger = MergeManager::new(&repo_path);
let diff = merger.diff(&branch).unwrap_or_default();
let commit_log = get_commit_log(&worktree_path);
let preview = merger.preview(&branch).ok();
let (files_changed, insertions, deletions, conflicts) = match preview {
Some(p) => (p.files_changed, p.insertions, p.deletions, p.conflicts),
None => (0, 0, 0, vec![]),
};
let diff_view = DiffView::new(diff).with_title(format!("{branch} → main"));
self.review_state = Some(ReviewState {
agent_index,
branch,
worktree_path,
diff_view,
commit_log,
files_changed,
insertions,
deletions,
conflicts,
focus: ReviewFocus::default(),
summary_scroll: 0,
});
self.mode = AppMode::ReviewMerge;
}
pub fn enqueue_merge(&mut self) {
let review = self.review_state.take();
let Some(review) = review else {
self.mode = AppMode::Normal;
return;
};
if review.agent_index != usize::MAX {
self.agent_manager.close(review.agent_index);
if review.agent_index < self.agent_issues.len() {
self.agent_issues.remove(review.agent_index);
}
if review.agent_index < self.agent_worktrees.len() {
self.agent_worktrees.remove(review.agent_index);
}
self.update_agent_sizes();
}
let task = MergeTask {
branch: review.branch.clone(),
worktree_path: review.worktree_path.clone(),
task_id: self.pending_review_task_id.take(),
};
let pending_count = self.merge_queue.pending_count();
self.merge_queue.enqueue(task);
self.add_notification(
format!(
"Merge queued: {} (pending: {})",
review.branch,
pending_count + 1
),
cctakt::plan::NotifyLevel::Info,
);
self.mode = AppMode::Normal;
self.process_merge_queue();
}
pub fn process_merge_queue(&mut self) {
if self.merge_queue.is_busy() {
return;
}
let branch = match self.merge_queue.start_next() {
Some(task) => task.branch.clone(),
None => return,
};
self.spawn_merge_worker(&branch);
}
fn spawn_merge_worker(&mut self, branch: &str) {
let repo_path = match env::current_dir() {
Ok(p) => p,
Err(e) => {
self.add_notification(
format!("Failed to get current directory: {e}"),
cctakt::plan::NotifyLevel::Error,
);
self.merge_queue.complete_current();
return;
}
};
let task_description = format!(
"mainブランチに {} をマージしてください。\n\n\
手順:\n\
1. git checkout main\n\
2. git pull origin main (最新を取得)\n\
3. git merge --no-ff {}\n\
4. コンフリクトがあれば解決してコミット\n\n\
重要: マージコミットを必ず作成してください。",
branch, branch
);
match self.agent_manager.add_non_interactive(
"merge-worker".to_string(),
repo_path,
&task_description,
Some(10), Some(branch.to_string()),
) {
Ok(agent_id) => {
let agent_index = self.agent_manager.len() - 1;
self.merge_queue.worker_agent_index = Some(agent_index);
self.update_agent_sizes();
self.add_notification(
format!("MergeWorker started (agent {})", agent_id),
cctakt::plan::NotifyLevel::Info,
);
}
Err(e) => {
self.add_notification(
format!("Failed to start MergeWorker: {e}"),
cctakt::plan::NotifyLevel::Error,
);
self.merge_queue.complete_current();
}
}
}
pub fn check_merge_worker_completion(&mut self) {
let Some(worker_idx) = self.merge_queue.worker_agent_index else {
return;
};
let Some(agent) = self.agent_manager.get(worker_idx) else {
return;
};
if agent.status != AgentStatus::Ended {
return;
}
let task = match self.merge_queue.current.take() {
Some(t) => t,
None => return,
};
let repo_path = match env::current_dir() {
Ok(p) => p,
Err(_) => {
self.handle_merge_failure(&task);
self.merge_queue.worker_agent_index = None;
self.process_merge_queue();
return;
}
};
let merged = std::process::Command::new("git")
.args([
"log",
"--oneline",
"-1",
"--grep",
&format!("Merge branch '{}'", task.branch),
])
.current_dir(&repo_path)
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
if merged {
self.handle_merge_success(&task);
} else {
self.handle_merge_failure(&task);
}
self.agent_manager.close(worker_idx);
self.merge_queue.worker_agent_index = None;
self.update_agent_sizes();
self.process_merge_queue();
}
fn handle_merge_success(&mut self, task: &MergeTask) {
self.add_notification(
format!("Merged: {} → main", task.branch),
cctakt::plan::NotifyLevel::Success,
);
if let Some(ref wt_manager) = self.worktree_manager {
let _ = wt_manager.remove(&task.worktree_path);
}
if let Some(ref task_id) = task.task_id {
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Completed);
let _ = self.plan_manager.save(plan);
}
}
self.spawn_build_worker(task.branch.clone());
}
fn handle_merge_failure(&mut self, task: &MergeTask) {
self.add_notification(
format!(
"Merge failed: {} (MergeWorker could not complete)",
task.branch
),
cctakt::plan::NotifyLevel::Error,
);
if let Some(ref task_id) = task.task_id {
if let Some(ref mut plan) = self.current_plan {
plan.mark_failed(task_id, "MergeWorker could not complete merge");
let _ = self.plan_manager.save(plan);
}
}
}
pub fn spawn_build_worker(&mut self, branch: String) {
let repo_path = match env::current_dir() {
Ok(p) => p,
Err(e) => {
self.add_notification(
format!("Failed to get current directory: {e}"),
cctakt::plan::NotifyLevel::Error,
);
return;
}
};
let task_description = "マージ後のビルドチェックを実行してください。\n\n\
手順:\n\
1. cargo build を実行\n\
2. エラーがあれば修正してコミット\n\
3. cargo test を実行(オプション)\n\n\
ビルドが成功したら完了です。"
.to_string();
match self.agent_manager.add_non_interactive(
"build-worker".to_string(),
repo_path,
&task_description,
Some(15), Some(branch.clone()),
) {
Ok(agent_id) => {
let agent_index = self.agent_manager.len() - 1;
self.build_worker_index = Some(agent_index);
self.build_worker_branch = Some(branch);
self.update_agent_sizes();
self.add_notification(
format!("BuildWorker started (agent {})", agent_id),
cctakt::plan::NotifyLevel::Info,
);
}
Err(e) => {
self.add_notification(
format!("Failed to start BuildWorker: {e}"),
cctakt::plan::NotifyLevel::Error,
);
}
}
}
pub fn check_build_worker_completion(&mut self) {
let Some(worker_idx) = self.build_worker_index else {
return;
};
let Some(agent) = self.agent_manager.get(worker_idx) else {
return;
};
if agent.status != AgentStatus::Ended {
return;
}
let build_success = agent.error.is_none();
let branch = self.build_worker_branch.take().unwrap_or_else(|| "unknown".to_string());
self.agent_manager.close(worker_idx);
self.build_worker_index = None;
self.update_agent_sizes();
if build_success {
self.add_notification(
format!("Build succeeded: {}", branch),
cctakt::plan::NotifyLevel::Success,
);
} else {
self.add_notification(
format!("Build failed: {}", branch),
cctakt::plan::NotifyLevel::Error,
);
}
}
pub fn cancel_review(&mut self) {
self.review_state = None;
self.mode = AppMode::Normal;
}
pub fn check_plan(&mut self) {
if self.plan_manager.has_changes() {
match self.plan_manager.load() {
Ok(Some(plan)) => {
if plan.is_complete() {
self.current_plan = None;
} else {
if let Some(desc) = &plan.description {
self.add_notification(
format!("Plan loaded: {desc}"),
cctakt::plan::NotifyLevel::Info,
);
}
self.current_plan = Some(plan);
}
}
Ok(None) => {
self.current_plan = None;
}
Err(e) => {
self.add_notification(
format!("Failed to load plan: {e}"),
cctakt::plan::NotifyLevel::Error,
);
}
}
}
}
pub fn process_plan(&mut self) {
self.recover_orphaned_tasks();
let next_task = self
.current_plan
.as_ref()
.and_then(|p| p.next_pending())
.cloned();
if let Some(task) = next_task {
self.execute_task(&task.id.clone());
}
if let Some(ref plan) = self.current_plan {
let _ = self.plan_manager.save(plan);
}
}
fn recover_orphaned_tasks(&mut self) {
let orphaned: Vec<String> = self
.current_plan
.as_ref()
.map(|plan| {
plan.tasks
.iter()
.filter(|t| t.status == TaskStatus::Running)
.filter(|t| !self.task_agents.contains_key(&t.id))
.map(|t| t.id.clone())
.collect()
})
.unwrap_or_default();
let notifications: Vec<String> = orphaned
.iter()
.filter_map(|task_id| {
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Pending);
Some(format!("Recovered orphaned task: {task_id}"))
} else {
None
}
})
.collect();
if !notifications.is_empty() {
self.save_plan();
}
for msg in notifications {
self.add_notification(msg, cctakt::plan::NotifyLevel::Warning);
}
}
fn execute_task(&mut self, task_id: &str) {
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Running);
}
self.save_plan();
let task_action = self
.current_plan
.as_ref()
.and_then(|p| p.get_task(task_id))
.map(|t| t.action.clone());
let Some(action) = task_action else {
return;
};
match action {
TaskAction::CreateWorker {
branch,
task_description,
base_branch,
} => {
self.execute_create_worker(task_id, &branch, &task_description, base_branch.as_deref());
}
TaskAction::CreatePr {
branch,
title,
body,
base,
draft,
} => {
self.execute_create_pr(
task_id,
&branch,
&title,
body.as_deref(),
base.as_deref(),
draft,
);
}
TaskAction::MergeBranch { branch, target } => {
self.execute_merge_branch(task_id, &branch, target.as_deref());
}
TaskAction::CleanupWorktree { worktree } => {
self.execute_cleanup_worktree(task_id, &worktree);
}
TaskAction::RunCommand { worktree, command } => {
self.execute_run_command(task_id, &worktree, &command);
}
TaskAction::Notify { message, level } => {
self.add_notification(message, level);
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Completed);
}
self.save_plan();
}
TaskAction::RequestReview { branch, after_task } => {
self.execute_request_review(task_id, &branch, after_task.as_deref());
}
}
}
fn execute_request_review(&mut self, task_id: &str, branch: &str, after_task: Option<&str>) {
if let Some(after_task_id) = after_task {
let after_completed = self
.current_plan
.as_ref()
.and_then(|p| p.get_task(after_task_id))
.map(|t| t.status == TaskStatus::Completed)
.unwrap_or(false);
if !after_completed {
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Pending);
}
self.save_plan();
return;
}
}
let agent_index = self.agent_worktrees.iter().position(|wt| {
wt.as_ref()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(|n| n == branch)
.unwrap_or(false)
});
if let Some(index) = agent_index {
self.pending_review_task_id = Some(task_id.to_string());
self.start_review(index);
} else {
let worktree_path = self.config.worktree_dir.join(branch);
if worktree_path.exists() {
self.pending_review_task_id = Some(task_id.to_string());
self.start_review_for_branch(branch, &worktree_path);
} else {
self.mark_task_failed(task_id, &format!("Branch '{branch}' not found"));
}
}
}
pub fn start_review_for_branch(&mut self, branch: &str, worktree_path: &PathBuf) {
let repo_path = env::current_dir().unwrap_or_default();
let merger = MergeManager::new(&repo_path);
let diff = merger.diff(branch).unwrap_or_default();
let commit_log = get_commit_log(worktree_path);
let preview = merger.preview(branch).ok();
let (files_changed, insertions, deletions, conflicts) = match preview {
Some(p) => (p.files_changed, p.insertions, p.deletions, p.conflicts),
None => (0, 0, 0, vec![]),
};
let diff_view = DiffView::new(diff).with_title(format!("{branch} → main"));
self.review_state = Some(ReviewState {
agent_index: usize::MAX, branch: branch.to_string(),
worktree_path: worktree_path.clone(),
diff_view,
commit_log,
files_changed,
insertions,
deletions,
conflicts,
focus: ReviewFocus::default(),
summary_scroll: 0,
});
self.mode = AppMode::ReviewMerge;
}
fn execute_create_worker(
&mut self,
task_id: &str,
branch: &str,
task_description: &str,
_base_branch: Option<&str>,
) {
let (working_dir, worktree_path) = if let Some(ref wt_manager) = self.worktree_manager {
match wt_manager.create(branch, &self.config.worktree_dir) {
Ok(path) => {
debug::log_worktree("created", &path);
(path.clone(), Some(path))
}
Err(e) => {
self.mark_task_failed(task_id, &format!("Failed to create worktree: {e}"));
return;
}
}
} else {
match env::current_dir() {
Ok(dir) => (dir, None),
Err(e) => {
self.mark_task_failed(task_id, &format!("Failed to get current directory: {e}"));
return;
}
}
};
let name = branch.to_string();
let full_prompt = format!(
"{}\n\n\
重要: 作業完了後は必ず git add と git commit を実行してコミットしてください。\n\
コミットせずに終了すると変更が失われます。",
task_description
);
match self.agent_manager.add_non_interactive(
name.clone(),
working_dir,
&full_prompt,
None, Some(branch.to_string()),
) {
Ok(_) => {
let agent_index = self.agent_manager.list().len() - 1;
self.agent_issues.push(None);
self.agent_worktrees.push(worktree_path);
self.task_agents.insert(task_id.to_string(), agent_index);
self.update_agent_sizes();
debug::log_task(task_id, "pending", "running");
self.add_notification(
format!("Worker started: {name}"),
cctakt::plan::NotifyLevel::Success,
);
}
Err(e) => {
self.mark_task_failed(task_id, &format!("Failed to create agent: {e}"));
}
}
}
fn execute_create_pr(
&mut self,
task_id: &str,
branch: &str,
title: &str,
body: Option<&str>,
base: Option<&str>,
draft: bool,
) {
let Some(ref client) = self.github_client else {
self.mark_task_failed(task_id, "GitHub client not configured");
return;
};
let create_req = cctakt::github::CreatePullRequest {
title: title.to_string(),
body: body.map(String::from),
head: branch.to_string(),
base: base.unwrap_or("main").to_string(),
draft,
};
match client.create_pull_request(&create_req) {
Ok(pr) => {
self.add_notification(
format!("PR created: #{} - {}", pr.number, pr.title),
cctakt::plan::NotifyLevel::Success,
);
let result = TaskResult {
commits: Vec::new(),
pr_number: Some(pr.number),
pr_url: Some(pr.html_url),
};
if let Some(ref mut plan) = self.current_plan {
plan.mark_completed(task_id, result);
if let Err(e) = self.plan_manager.save(plan) {
debug::log(&format!("Failed to save plan: {e}"));
}
}
}
Err(e) => {
self.mark_task_failed(task_id, &format!("Failed to create PR: {e}"));
}
}
}
fn execute_merge_branch(&mut self, task_id: &str, branch: &str, target: Option<&str>) {
let repo_path = match env::current_dir() {
Ok(p) => p,
Err(e) => {
self.mark_task_failed(task_id, &format!("Failed to get current directory: {e}"));
return;
}
};
let merger = MergeManager::new(&repo_path);
let merger = if let Some(t) = target {
merger.with_main_branch(t)
} else {
merger
};
match merger.merge_no_ff(branch, None) {
Ok(()) => {
self.add_notification(
format!("Merged: {} → {}", branch, target.unwrap_or("main")),
cctakt::plan::NotifyLevel::Success,
);
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Completed);
}
self.save_plan();
}
Err(e) => {
self.mark_task_failed(task_id, &format!("Failed to merge: {e}"));
}
}
}
fn execute_cleanup_worktree(&mut self, task_id: &str, worktree: &str) {
if let Some(ref wt_manager) = self.worktree_manager {
let worktree_path = self.config.worktree_dir.join(worktree);
match wt_manager.remove(&worktree_path) {
Ok(()) => {
self.add_notification(
format!("Worktree cleaned up: {worktree}"),
cctakt::plan::NotifyLevel::Info,
);
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Completed);
}
self.save_plan();
}
Err(e) => {
self.mark_task_failed(task_id, &format!("Failed to cleanup worktree: {e}"));
}
}
} else {
self.mark_task_failed(task_id, "Worktree manager not available");
}
}
fn execute_run_command(&mut self, task_id: &str, worktree: &str, command: &str) {
self.add_notification(
format!("RunCommand not implemented: {command} in {worktree}"),
cctakt::plan::NotifyLevel::Warning,
);
if let Some(ref mut plan) = self.current_plan {
plan.update_status(task_id, TaskStatus::Skipped);
}
self.save_plan();
}
pub fn mark_task_failed(&mut self, task_id: &str, error: &str) {
self.add_notification(
format!("Task failed: {error}"),
cctakt::plan::NotifyLevel::Error,
);
if let Some(ref mut plan) = self.current_plan {
plan.mark_failed(task_id, error);
if let Err(e) = self.plan_manager.save(plan) {
debug::log(&format!("Failed to save plan: {e}"));
}
}
}
pub fn add_notification(&mut self, message: String, level: cctakt::plan::NotifyLevel) {
self.notifications.push(Notification {
message,
level,
created_at: std::time::Instant::now(),
});
}
pub fn save_plan(&mut self) {
if let Some(ref plan) = self.current_plan {
if let Err(e) = self.plan_manager.save(plan) {
debug::log(&format!("Failed to save plan: {e}"));
}
}
}
pub fn cleanup_notifications(&mut self) {
let now = std::time::Instant::now();
self.notifications
.retain(|n| now.duration_since(n.created_at).as_secs() < 5);
}
pub fn check_agent_task_completions(&mut self) {
let ended: Vec<(String, usize, Option<String>)> = self
.task_agents
.iter()
.filter_map(|(task_id, &agent_index)| {
self.agent_manager
.get(agent_index)
.filter(|a| a.status == AgentStatus::Ended)
.map(|a| (task_id.clone(), agent_index, a.error.clone()))
})
.collect();
for (task_id, agent_index, error) in ended {
if let Some(error_msg) = error {
debug::log_task(&task_id, "running", "failed");
self.add_notification(
format!("Worker failed: {error_msg}"),
cctakt::plan::NotifyLevel::Error,
);
if let Some(ref mut plan) = self.current_plan {
plan.mark_failed(&task_id, &error_msg);
if let Err(e) = self.plan_manager.save(plan) {
debug::log(&format!("Failed to save plan: {e}"));
}
}
} else {
let commits = if agent_index < self.agent_worktrees.len() {
if let Some(ref worktree_path) = self.agent_worktrees[agent_index] {
get_worker_commits(worktree_path)
} else {
Vec::new()
}
} else {
Vec::new()
};
if commits.is_empty() {
self.add_notification(
format!("Worker {task_id} completed with no commits"),
cctakt::plan::NotifyLevel::Warning,
);
}
let result = TaskResult {
commits,
pr_number: None,
pr_url: None,
};
if let Some(ref mut plan) = self.current_plan {
plan.mark_completed(&task_id, result);
if let Err(e) = self.plan_manager.save(plan) {
debug::log(&format!("Failed to save plan: {e}"));
}
}
debug::log_task(&task_id, "running", "completed");
}
self.task_agents.remove(&task_id);
}
}
pub fn resize(&mut self, cols: u16, rows: u16) {
self.content_cols = cols;
self.content_rows = rows;
self.update_agent_sizes();
}
pub fn update_agent_sizes(&mut self) {
let has_workers = self.agent_manager.has_non_interactive();
if has_workers {
let pane_width = (self.content_cols.saturating_sub(1)) / 2;
if let Some(agent) = self.agent_manager.get_interactive_mut() {
agent.resize(pane_width, self.content_rows);
}
for agent in self.agent_manager.get_all_non_interactive_mut() {
agent.resize(pane_width, self.content_rows);
}
} else {
self.agent_manager.resize_all(self.content_cols, self.content_rows);
}
}
pub fn restart_conductor(&mut self) -> Result<()> {
self.agent_manager.restart_interactive(self.content_rows, self.content_cols)
}
}