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