git_paw/coordination/
tell.rs1use crate::config::TellMode;
10
11use super::inventory::Mode;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct TellCommand {
16 pub target_id: String,
18 pub prompt: String,
21}
22
23#[must_use]
30pub fn parse_tell(input: &str) -> Option<TellCommand> {
31 let trimmed = input.trim_start();
32 let rest = trimmed.strip_prefix("/tell")?;
33 if !rest.starts_with(char::is_whitespace) {
36 return None;
37 }
38 let rest = rest.trim_start();
39 let mut chars = rest.char_indices();
43 let target_end = chars
44 .find(|(_, c)| c.is_whitespace())
45 .map_or(rest.len(), |(i, _)| i);
46 let target_id = rest[..target_end].to_string();
47 if target_id.is_empty() {
48 return None;
49 }
50 let prompt = rest[target_end..].trim().to_string();
51 if prompt.is_empty() {
52 return None;
53 }
54 Some(TellCommand { target_id, prompt })
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum DeliveryDecision {
60 Feedback,
62 SendKeys,
64 FeedbackFallback,
67}
68
69impl DeliveryDecision {
70 #[must_use]
73 pub fn uses_feedback(self) -> bool {
74 matches!(self, Self::Feedback | Self::FeedbackFallback)
75 }
76
77 #[must_use]
80 pub fn is_fallback(self) -> bool {
81 matches!(self, Self::FeedbackFallback)
82 }
83
84 #[must_use]
86 pub fn learnings_label(self) -> &'static str {
87 match self {
88 Self::Feedback | Self::FeedbackFallback => "feedback",
89 Self::SendKeys => "send-keys",
90 }
91 }
92}
93
94#[must_use]
101pub fn select_delivery_mode(configured: TellMode, detected: Mode) -> DeliveryDecision {
102 match configured {
103 TellMode::SendKeys => match detected {
104 Mode::AcceptEdits => DeliveryDecision::SendKeys,
105 Mode::Interactive | Mode::Unknown => DeliveryDecision::FeedbackFallback,
106 },
107 TellMode::Feedback => DeliveryDecision::Feedback,
108 }
109}
110
111#[must_use]
114pub fn fallback_note(target_id: &str, detected: Mode) -> String {
115 format!(
116 "note: [supervisor.tell] mode = \"send-keys\" but target `{target_id}` detected mode is \
117 `{detected}`; falling back to agent.feedback delivery. Check the agent's mode if you \
118 expected direct keystroke injection."
119 )
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn parse_tell_basic() {
128 let cmd = parse_tell("/tell feat/auth rebase onto main").unwrap();
129 assert_eq!(cmd.target_id, "feat/auth");
130 assert_eq!(cmd.prompt, "rebase onto main");
131 }
132
133 #[test]
134 fn parse_tell_multiline_prompt_preserved() {
135 let cmd = parse_tell("/tell feat-api\nrun the migration\nthen restart").unwrap();
136 assert_eq!(cmd.target_id, "feat-api");
137 assert_eq!(cmd.prompt, "run the migration\nthen restart");
138 }
139
140 #[test]
141 fn parse_tell_rejects_non_directive() {
142 assert!(parse_tell("hello there").is_none());
143 assert!(parse_tell("/tellfoo bar").is_none());
144 }
145
146 #[test]
147 fn parse_tell_rejects_missing_prompt_or_target() {
148 assert!(parse_tell("/tell feat-auth").is_none());
149 assert!(parse_tell("/tell ").is_none());
150 assert!(parse_tell("/tell").is_none());
151 }
152
153 #[test]
154 fn default_feedback_mode_uses_feedback() {
155 for detected in [Mode::AcceptEdits, Mode::Interactive, Mode::Unknown] {
156 assert_eq!(
157 select_delivery_mode(TellMode::Feedback, detected),
158 DeliveryDecision::Feedback
159 );
160 }
161 }
162
163 #[test]
164 fn send_keys_mode_targets_accept_edits() {
165 assert_eq!(
166 select_delivery_mode(TellMode::SendKeys, Mode::AcceptEdits),
167 DeliveryDecision::SendKeys
168 );
169 }
170
171 #[test]
172 fn send_keys_falls_back_for_non_accept_edits() {
173 assert_eq!(
174 select_delivery_mode(TellMode::SendKeys, Mode::Interactive),
175 DeliveryDecision::FeedbackFallback
176 );
177 assert_eq!(
178 select_delivery_mode(TellMode::SendKeys, Mode::Unknown),
179 DeliveryDecision::FeedbackFallback
180 );
181 }
182
183 #[test]
184 fn decision_helpers() {
185 assert!(DeliveryDecision::Feedback.uses_feedback());
186 assert!(DeliveryDecision::FeedbackFallback.uses_feedback());
187 assert!(!DeliveryDecision::SendKeys.uses_feedback());
188 assert!(DeliveryDecision::FeedbackFallback.is_fallback());
189 assert!(!DeliveryDecision::Feedback.is_fallback());
190 assert_eq!(DeliveryDecision::SendKeys.learnings_label(), "send-keys");
191 assert_eq!(
192 DeliveryDecision::FeedbackFallback.learnings_label(),
193 "feedback"
194 );
195 }
196
197 #[test]
198 fn fallback_note_names_target_and_mode() {
199 let note = fallback_note("feat-auth", Mode::Unknown);
200 assert!(note.contains("feat-auth"));
201 assert!(note.contains("unknown"));
202 assert!(note.contains("agent.feedback"));
203 }
204}