Skip to main content

agent_first_mail/runner/
dispatch.rs

1use super::lock::{command_lock, workspace_progress_command};
2use super::purge::{resolve_purge_action, ResolvedPurgeAction};
3use super::push::push_confirmed;
4use crate::cli::{
5    ArchiveAction, ArchiveCaseAction, ArchiveCaseNotesAction, ArchiveListAction,
6    ArchiveMessageCommand, ArchiveMessageNotesAction, CaseCommand, CaseDraftAction,
7    CaseNotesAction, Command, ConfigAction, DoctorAction, LogAction, MessageAction,
8    MessageAttachmentAction, PushAction, RemoteAction, RenderAction, TriageAction,
9};
10use crate::error::{AppError, Result};
11use crate::progress::{ProgressCallback, WorkspaceProgressSink};
12use crate::store::{DraftChange, Workspace};
13use crate::workspace_lock::{LockMode, WorkspaceLock};
14use serde_json::{json, Value};
15use std::path::Path;
16
17pub fn execute_command(command: Command) -> Result<Value> {
18    execute_command_with_progress(command, None)
19}
20
21pub(super) fn execute_command_with_progress(
22    command: Command,
23    progress: Option<&mut ProgressCallback<'_>>,
24) -> Result<Value> {
25    match command {
26        Command::Skill { action } => crate::skill_admin::handle_action(action),
27        Command::Status => {
28            let cwd = std::env::current_dir().map_err(|e| AppError::io("current dir", &e))?;
29            execute_status(&cwd)
30        }
31        Command::Init { .. } => {
32            let cwd = std::env::current_dir().map_err(|e| AppError::io("current dir", &e))?;
33            execute_command_unlocked(command, &cwd, progress)
34        }
35        command => {
36            let cwd = std::env::current_dir().map_err(|e| AppError::io("current dir", &e))?;
37            let lock = command_lock(&command, &cwd)?;
38            let _lock = WorkspaceLock::acquire(&lock.root, lock.mode)?;
39            let mut workspace_progress = workspace_progress_command(&command)
40                .map(|name| WorkspaceProgressSink::start(&lock.root, name));
41            if lock.mode == LockMode::Exclusive
42                && !matches!(command, Command::Init { .. } | Command::Doctor { .. })
43            {
44                if let Err(err) = Workspace::at(&lock.root).ensure_no_incomplete_transactions() {
45                    if let Some(sink) = &mut workspace_progress {
46                        sink.finish_failure(&err);
47                    }
48                    return Err(err);
49                }
50            }
51            let mut progress = progress;
52            let should_emit_progress = progress.is_some() || workspace_progress.is_some();
53            let result = {
54                let mut emit_progress = |phase: &str, fields: Value| {
55                    if let Some(callback) = progress.as_deref_mut() {
56                        callback(phase, fields.clone());
57                    }
58                    if let Some(sink) = &mut workspace_progress {
59                        sink.update(phase, fields);
60                    }
61                };
62                let progress = if should_emit_progress {
63                    Some(&mut emit_progress as &mut ProgressCallback<'_>)
64                } else {
65                    None
66                };
67                execute_command_unlocked(command, &cwd, progress)
68            };
69            if let Some(sink) = &mut workspace_progress {
70                match &result {
71                    Ok(value) => sink.finish_success(value),
72                    Err(err) => sink.finish_failure(err),
73                }
74            }
75            result
76        }
77    }
78}
79
80fn execute_command_unlocked(
81    command: Command,
82    cwd: &Path,
83    progress: Option<&mut ProgressCallback<'_>>,
84) -> Result<Value> {
85    match command {
86        Command::Init { path } => Workspace::init_command(cwd, path.as_deref()),
87        Command::Pull { ids } => Workspace::discover(cwd)?.pull_with_progress(&ids, progress),
88        Command::Config { action } => {
89            let ws = Workspace::discover(cwd)?;
90            match action {
91                ConfigAction::Show => ws.config_show(),
92                ConfigAction::Get { key } => ws.config_get(&key),
93                ConfigAction::Set { key, values } => ws.config_set(&key, &values),
94            }
95        }
96        Command::Remote { action } => {
97            let ws = Workspace::discover(cwd)?;
98            match action {
99                RemoteAction::Test => ws.remote_test(),
100                RemoteAction::Folders => ws.remote_folders(),
101            }
102        }
103        Command::Push {
104            dry_run,
105            confirm,
106            action,
107        } => {
108            let ws = Workspace::discover(cwd)?;
109            match action {
110                Some(PushAction::List) => ws.push_list(),
111                None => {
112                    let confirmed = push_confirmed(dry_run, confirm)?;
113                    ws.push_with_progress(confirmed, progress)
114                }
115            }
116        }
117        Command::Status => execute_status(cwd),
118        Command::Doctor { action } => {
119            let ws = Workspace::discover(cwd)?;
120            match action {
121                None => ws.doctor(),
122                Some(DoctorAction::Repair { confirm }) => ws.doctor_repair(confirm),
123            }
124        }
125        Command::Purge {
126            action,
127            older_than_days,
128        } => {
129            let ws = Workspace::discover(cwd)?;
130            match resolve_purge_action(action, older_than_days)? {
131                ResolvedPurgeAction::Spam { older_than_days } => ws.purge_spam(older_than_days),
132                ResolvedPurgeAction::Trash { older_than_days } => ws.purge_trash(older_than_days),
133                ResolvedPurgeAction::Deleted { older_than_days } => {
134                    ws.purge_deleted(older_than_days)
135                }
136                ResolvedPurgeAction::Discards { older_than_days } => {
137                    ws.purge_discards(older_than_days)
138                }
139            }
140        }
141        Command::Skill { action } => crate::skill_admin::handle_action(action),
142        Command::Triage { action } => {
143            let ws = Workspace::discover(cwd)?;
144            match action {
145                TriageAction::List => ws.triage_list(),
146            }
147        }
148        Command::Message { action } => {
149            let ws = Workspace::discover(cwd)?;
150            match action {
151                MessageAction::Show { message_id } => ws.message_show(&message_id),
152                MessageAction::Spam { message_id, reason } => {
153                    ws.spam_message(&message_id, reason.as_deref())
154                }
155                MessageAction::Trash { message_id, reason } => {
156                    ws.trash_message(&message_id, reason.as_deref())
157                }
158                MessageAction::Restore { message_id, reason } => {
159                    ws.restore_message(&message_id, reason.as_deref())
160                }
161                MessageAction::Attachment { action } => match action {
162                    MessageAttachmentAction::Fetch {
163                        message_id,
164                        part_id,
165                    } => ws.fetch_message_attachment(&message_id, part_id.as_deref()),
166                },
167            }
168        }
169        Command::Case { action } => {
170            let ws = Workspace::discover(cwd)?;
171            match action {
172                CaseCommand::Create(args) => ws.create_case(
173                    &args.name,
174                    args.group.as_deref(),
175                    args.message.as_deref(),
176                    args.summary.as_deref(),
177                    args.reason.as_deref(),
178                ),
179                CaseCommand::List => ws.case_list(),
180                CaseCommand::Show { case_ref } => ws.active_case_show(&case_ref),
181                CaseCommand::Add {
182                    case_ref,
183                    message_id,
184                    summary,
185                    reason,
186                } => ws.add_message_to_case(
187                    &case_ref,
188                    &message_id,
189                    summary.as_deref(),
190                    reason.as_deref(),
191                ),
192                CaseCommand::Move { case_ref, group } => ws.move_case(&case_ref, &group),
193                CaseCommand::Rename {
194                    case_ref,
195                    name,
196                    reason,
197                } => ws.rename_active_case(&case_ref, &name, reason.as_deref()),
198                CaseCommand::Notes { action } => match action {
199                    CaseNotesAction::Show { case_ref } => ws.active_case_notes_show(&case_ref),
200                    CaseNotesAction::Append { case_ref, text } => {
201                        ws.active_case_notes_append(&case_ref, &text)
202                    }
203                    CaseNotesAction::Replace { case_ref, text } => {
204                        ws.active_case_notes_replace(&case_ref, &text)
205                    }
206                },
207                CaseCommand::Archive { case_ref, reason } => {
208                    ws.archive_case(&case_ref, reason.as_deref())
209                }
210                CaseCommand::Reopen { case_ref, reason } => {
211                    ws.reopen_case(&case_ref, reason.as_deref())
212                }
213                CaseCommand::Tag {
214                    case_ref,
215                    tag,
216                    reason,
217                } => ws.tag_case(&case_ref, &tag, reason.as_deref()),
218                CaseCommand::Untag {
219                    case_ref,
220                    tag,
221                    reason,
222                } => ws.untag_case(&case_ref, &tag, reason.as_deref()),
223                CaseCommand::Draft { action } => match action {
224                    CaseDraftAction::New {
225                        case_ref,
226                        to,
227                        cc,
228                        subject,
229                        body,
230                        body_file,
231                    } => ws.create_draft(
232                        &case_ref,
233                        &to,
234                        &cc,
235                        &subject,
236                        body.as_deref(),
237                        body_file.as_deref(),
238                    ),
239                    CaseDraftAction::Reply {
240                        case_ref,
241                        message_id,
242                        body,
243                        body_file,
244                        all,
245                    } => ws.reply_to_message(
246                        &case_ref,
247                        &message_id,
248                        all,
249                        body.as_deref(),
250                        body_file.as_deref(),
251                    ),
252                    CaseDraftAction::Change {
253                        case_ref,
254                        draft_name,
255                        subject,
256                        to,
257                        cc,
258                        clear_cc,
259                        body,
260                        body_file,
261                    } => ws.change_draft(
262                        &case_ref,
263                        &draft_name,
264                        DraftChange {
265                            subject: subject.as_deref(),
266                            to: &to,
267                            cc: &cc,
268                            clear_cc,
269                            body: body.as_deref(),
270                            body_file: body_file.as_deref(),
271                        },
272                    ),
273                    CaseDraftAction::Show {
274                        case_ref,
275                        draft_name,
276                    } => ws.show_draft(&case_ref, &draft_name),
277                    CaseDraftAction::Validate {
278                        case_ref,
279                        draft_name,
280                    } => ws.validate_draft(&case_ref, &draft_name),
281                    CaseDraftAction::Save {
282                        case_ref,
283                        draft_name,
284                    } => ws.save_draft(&case_ref, &draft_name),
285                    CaseDraftAction::Send {
286                        case_ref,
287                        draft_name,
288                    } => ws.send_draft(&case_ref, &draft_name),
289                    CaseDraftAction::Attach {
290                        case_ref,
291                        draft_name,
292                        path,
293                    } => ws.attach_file_to_draft(&case_ref, &draft_name, &path),
294                    CaseDraftAction::Remove {
295                        case_ref,
296                        draft_name,
297                        reason,
298                    } => ws.remove_draft(&case_ref, &draft_name, reason.as_deref()),
299                },
300                CaseCommand::Merge {
301                    case_ref,
302                    other_case_ref,
303                    reason,
304                } => ws.merge_case(&case_ref, &other_case_ref, reason.as_deref()),
305            }
306        }
307        Command::Archive { action } => {
308            let ws = Workspace::discover(cwd)?;
309            match action {
310                ArchiveAction::List { target: None } => ws.archive_list(),
311                ArchiveAction::List {
312                    target: Some(ArchiveListAction::Cases),
313                } => ws.archive_list_cases(),
314                ArchiveAction::List {
315                    target: Some(ArchiveListAction::Messages),
316                } => ws.archive_list_messages(),
317                ArchiveAction::Message { action } => match action {
318                    ArchiveMessageCommand::Create(args) => ws.create_archive_message_category(
319                        &args.name,
320                        args.message.as_deref(),
321                        args.summary.as_deref(),
322                        args.reason.as_deref(),
323                    ),
324                    ArchiveMessageCommand::Add {
325                        archive_ref,
326                        message_id,
327                        summary,
328                        reason,
329                    } => ws.archive_message(
330                        &message_id,
331                        &archive_ref,
332                        Some(summary.as_str()),
333                        reason.as_deref(),
334                    ),
335                    ArchiveMessageCommand::Show { archive_ref } => {
336                        ws.archive_message_show(&archive_ref)
337                    }
338                    ArchiveMessageCommand::Restore {
339                        archive_ref,
340                        message_id,
341                        reason,
342                    } => ws.archive_message_restore(&archive_ref, &message_id, reason.as_deref()),
343                    ArchiveMessageCommand::Move {
344                        archive_ref,
345                        message_id,
346                        new_archive_ref,
347                        reason,
348                    } => ws.archive_message_move(
349                        &archive_ref,
350                        &message_id,
351                        &new_archive_ref,
352                        reason.as_deref(),
353                    ),
354                    ArchiveMessageCommand::Rename {
355                        archive_ref,
356                        name,
357                        reason,
358                    } => ws.archive_message_rename(&archive_ref, &name, reason.as_deref()),
359                    ArchiveMessageCommand::SetSummary {
360                        archive_ref,
361                        message_id,
362                        summary,
363                        reason,
364                    } => ws.archive_message_set_summary(
365                        &archive_ref,
366                        &message_id,
367                        &summary,
368                        reason.as_deref(),
369                    ),
370                    ArchiveMessageCommand::Notes { action } => match action {
371                        ArchiveMessageNotesAction::Show { archive_ref } => {
372                            ws.archive_message_notes_show(&archive_ref)
373                        }
374                        ArchiveMessageNotesAction::Append { archive_ref, text } => {
375                            ws.archive_message_notes_append(&archive_ref, &text)
376                        }
377                        ArchiveMessageNotesAction::Replace { archive_ref, text } => {
378                            ws.archive_message_notes_replace(&archive_ref, &text)
379                        }
380                    },
381                },
382                ArchiveAction::Case { action } => match action {
383                    ArchiveCaseAction::Show { case_ref } => ws.archive_case_show(&case_ref),
384                    ArchiveCaseAction::Restore {
385                        case_ref,
386                        group,
387                        reason,
388                    } => ws.archive_case_restore(&case_ref, &group, reason.as_deref()),
389                    ArchiveCaseAction::Rename {
390                        case_ref,
391                        name,
392                        reason,
393                    } => ws.archive_case_rename(&case_ref, &name, reason.as_deref()),
394                    ArchiveCaseAction::Notes { action } => match action {
395                        ArchiveCaseNotesAction::Show { case_ref } => {
396                            ws.archive_case_notes_show(&case_ref)
397                        }
398                        ArchiveCaseNotesAction::Append { case_ref, text } => {
399                            ws.archive_case_notes_append(&case_ref, &text)
400                        }
401                        ArchiveCaseNotesAction::Replace { case_ref, text } => {
402                            ws.archive_case_notes_replace(&case_ref, &text)
403                        }
404                    },
405                },
406            }
407        }
408        Command::Render { action } => {
409            let ws = Workspace::discover(cwd)?;
410            match action {
411                RenderAction::Refresh => ws.render_refresh(),
412                RenderAction::Templates { force } => ws.render_templates(force),
413            }
414        }
415        Command::Log { action } => {
416            let ws = Workspace::discover(cwd)?;
417            match action {
418                LogAction::List { limit } => ws.log_list(limit),
419                LogAction::Tail => ws.log_tail(),
420                LogAction::Message { message_id } => ws.log_message(&message_id),
421                LogAction::Case { case_uid } => ws.log_case(&case_uid),
422                LogAction::Archive { archive_uid } => ws.log_archive(&archive_uid),
423            }
424        }
425        #[cfg(feature = "ui")]
426        Command::Ui(_) => {
427            let ws = Workspace::discover(cwd)?;
428            let message = crate::ui::workspace_snapshot(&ws)?;
429            serde_json::to_value(message).map_err(|e| AppError::json("serialize AFUI snapshot", &e))
430        }
431    }
432}
433
434fn execute_status(cwd: &Path) -> Result<Value> {
435    let ws = Workspace::discover(cwd)?;
436    let progress = crate::progress::workspace_status_progress(ws.root())?;
437    let Some(_lock) = WorkspaceLock::try_acquire(ws.root(), LockMode::Shared)? else {
438        return Ok(json!({
439            "code": "status",
440            "workspace_locked": true,
441            "progress": progress,
442            "hint": "Workspace counts are omitted while another afmail command is using this workspace; retry after it finishes for full counts."
443        }));
444    };
445    let mut status = ws.status()?;
446    if let Value::Object(map) = &mut status {
447        map.insert("progress".to_string(), progress);
448    }
449    Ok(status)
450}