| 1 | //! `/relay` command. |
| 2 | |
| 3 | use std::fmt::Write as _; |
| 4 | |
| 5 | use crate::commands::traits::{CommandInfo, RegisterCommand}; |
| 6 | use crate::localization::MessageId; |
| 7 | use crate::tui::app::{App, AppAction}; |
| 8 | |
| 9 | use super::CommandResult; |
| 10 | |
| 11 | pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo { |
| 12 | name: "relay", |
| 13 | aliases: &["batonpass", "接力"], |
| 14 | usage: "/relay [focus]", |
| 15 | description_id: MessageId::CmdRelayDescription, |
| 16 | }; |
| 17 | |
| 18 | pub(in crate::commands) struct RelayCmd; |
| 19 | |
| 20 | impl RegisterCommand for RelayCmd { |
| 21 | fn info() -> &'static CommandInfo { |
| 22 | &COMMAND_INFO |
| 23 | } |
| 24 | |
| 25 | fn execute(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 26 | relay(app, arg) |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | /// Ask the active model to write a compact relay artifact for the next thread. |
| 31 | /// |
| 32 | /// The visible command is `/relay` (with `/接力` for Chinese users), but the |
| 33 | /// durable file path remains `.deepseek/handoff.md` for compatibility with |
| 34 | /// existing sessions and startup prompt loading. |
| 35 | pub fn relay(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 36 | let focus = arg.map(str::trim).filter(|value| !value.is_empty()); |
| 37 | let message = build_relay_instruction(app, focus); |
| 38 | CommandResult::with_message_and_action( |
| 39 | "Preparing session relay at .deepseek/handoff.md...", |
| 40 | AppAction::SendMessage(message), |
| 41 | ) |
| 42 | } |
| 43 | |
| 44 | fn build_relay_instruction(app: &App, focus: Option<&str>) -> String { |
| 45 | let mut out = String::new(); |
| 46 | let _ = writeln!( |
| 47 | out, |
| 48 | "Create a compact session relay (接力) for a future Codewhale thread." |
| 49 | ); |
| 50 | let _ = writeln!(out); |
| 51 | let _ = writeln!(out, "Write or update `.deepseek/handoff.md`."); |
| 52 | let _ = writeln!( |
| 53 | out, |
| 54 | "Keep the existing file path for compatibility, but title the artifact `# Session relay`." |
| 55 | ); |
| 56 | let _ = writeln!(out); |
| 57 | let _ = writeln!(out, "Use this relay structure:"); |
| 58 | let _ = writeln!(out); |
| 59 | let _ = writeln!(out, "{}", crate::prompts::COMPACT_TEMPLATE.trim()); |
| 60 | let _ = writeln!(out); |
| 61 | let _ = writeln!(out, "Current session snapshot:"); |
| 62 | let _ = writeln!(out, "- Workspace: {}", app.workspace.display()); |
| 63 | let _ = writeln!(out, "- Mode: {}", app.mode.label()); |
| 64 | let _ = writeln!(out, "- Model: {}", app.model_display_label()); |
| 65 | if let Some(focus) = focus { |
| 66 | let _ = writeln!(out, "- Requested relay focus: {focus}"); |
| 67 | } |
| 68 | if let Some(quarry) = app.hunt.quarry.as_deref() { |
| 69 | let _ = writeln!(out, "- Goal objective: {quarry}"); |
| 70 | } |
| 71 | if let Some(budget) = app.hunt.token_budget { |
| 72 | let _ = writeln!(out, "- Goal token budget: {budget}"); |
| 73 | } |
| 74 | // Read through the same authoritative graph-backed snapshot seam used by |
| 75 | // persistence. A model can call `work_update` immediately before `/relay`; |
| 76 | // the UI projection may not have published yet, but the handoff must still |
| 77 | // describe the staged Work state rather than a stale compatibility list. |
| 78 | match app.work_state_snapshot() { |
| 79 | Ok(Some(state)) => { |
| 80 | if let Some(body) = crate::work_grounding::canonical_todo_body(&state.todos) { |
| 81 | let _ = writeln!(out, "\nCurrent Work state (the To-do ledger is canonical):"); |
| 82 | let _ = writeln!(out, "{body}"); |
| 83 | } |
| 84 | } |
| 85 | Ok(None) => {} |
| 86 | Err(_) => { |
| 87 | let _ = writeln!(out, "\nTo-do: unavailable because the list is busy."); |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | if let Ok(plan) = app.plan_state.try_lock() { |
| 92 | let snapshot = plan.snapshot(); |
| 93 | if !snapshot.is_empty() { |
| 94 | let _ = writeln!( |
| 95 | out, |
| 96 | "\nConversational strategy notes from update_plan (reasoning context, not a Work surface):" |
| 97 | ); |
| 98 | write_plan_field(&mut out, "Title", snapshot.title.as_deref()); |
| 99 | write_plan_field(&mut out, "Objective", snapshot.objective.as_deref()); |
| 100 | write_plan_field(&mut out, "Context", snapshot.context_summary.as_deref()); |
| 101 | write_plan_field(&mut out, "Explanation", snapshot.explanation.as_deref()); |
| 102 | write_plan_list(&mut out, "Source", &snapshot.sources_used); |
| 103 | write_plan_list(&mut out, "Critical file", &snapshot.critical_files); |
| 104 | write_plan_list(&mut out, "Constraint", &snapshot.constraints); |
| 105 | write_plan_field( |
| 106 | &mut out, |
| 107 | "Recommended approach", |
| 108 | snapshot.recommended_approach.as_deref(), |
| 109 | ); |
| 110 | write_plan_field( |
| 111 | &mut out, |
| 112 | "Verification plan", |
| 113 | snapshot.verification_plan.as_deref(), |
| 114 | ); |
| 115 | write_plan_field( |
| 116 | &mut out, |
| 117 | "Risks and unknowns", |
| 118 | snapshot.risks_and_unknowns.as_deref(), |
| 119 | ); |
| 120 | write_plan_field( |
| 121 | &mut out, |
| 122 | "Handoff packet", |
| 123 | snapshot.handoff_packet.as_deref(), |
| 124 | ); |
| 125 | for item in snapshot.items { |
| 126 | let _ = writeln!(out, "- [{}] {}", plan_status_label(&item.status), item.step); |
| 127 | } |
| 128 | } |
| 129 | } else { |
| 130 | let _ = writeln!( |
| 131 | out, |
| 132 | "\nStrategy metadata: unavailable because plan state is busy." |
| 133 | ); |
| 134 | } |
| 135 | |
| 136 | let _ = writeln!( |
| 137 | out, |
| 138 | "\nBefore writing, inspect the current transcript context and any live tool evidence you need. Do not invent test results, file changes, blockers, or decisions." |
| 139 | ); |
| 140 | let _ = writeln!( |
| 141 | out, |
| 142 | "\nKeep it under about 900 words unless the session genuinely needs more. After writing, report the path and the single next action." |
| 143 | ); |
| 144 | out |
| 145 | } |
| 146 | |
| 147 | fn write_plan_field(out: &mut String, label: &str, value: Option<&str>) { |
| 148 | if let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) { |
| 149 | let _ = writeln!(out, "- {label}: {value}"); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | fn write_plan_list(out: &mut String, label: &str, values: &[String]) { |
| 154 | for value in values { |
| 155 | let value = value.trim(); |
| 156 | if !value.is_empty() { |
| 157 | let _ = writeln!(out, "- {label}: {value}"); |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | fn plan_status_label(status: &crate::tools::plan::StepStatus) -> &'static str { |
| 163 | match status { |
| 164 | crate::tools::plan::StepStatus::Pending => "pending", |
| 165 | crate::tools::plan::StepStatus::InProgress => "in_progress", |
| 166 | crate::tools::plan::StepStatus::Completed => "completed", |
| 167 | } |
| 168 | } |
| 169 |