1use crate::agent::{Agent, ModelSize};
3use crate::output::AgentOutput;
4use crate::providers::common::CommonAgentState;
5use crate::session_log::{
6 BackfilledSession, HistoricalLogAdapter, LiveLogAdapter, LiveLogContext, LogCompleteness,
7 LogEventKind, LogSourceKind, SessionLogMetadata, SessionLogWriter, ToolKind,
8};
9use anyhow::{Context, Result};
10
11fn tool_kind_from_name(name: &str) -> ToolKind {
13 match name {
14 "bash" | "shell" => ToolKind::Shell,
15 "view" | "read" | "cat" => ToolKind::FileRead,
16 "write" => ToolKind::FileWrite,
17 "edit" | "insert" | "replace" => ToolKind::FileEdit,
18 "grep" | "glob" | "find" | "search" => ToolKind::Search,
19 _ => ToolKind::Other,
20 }
21}
22use async_trait::async_trait;
23use log::info;
24use std::collections::HashSet;
25use std::io::{BufRead, BufReader, Seek, SeekFrom};
26use std::path::{Path, PathBuf};
27use tokio::fs;
28use tokio::process::Command;
29
30pub fn session_state_dir() -> std::path::PathBuf {
32 dirs::home_dir()
33 .unwrap_or_else(|| std::path::PathBuf::from("."))
34 .join(".copilot/session-state")
35}
36
37pub const DEFAULT_MODEL: &str = "claude-sonnet-4.6";
38
39pub const AVAILABLE_MODELS: &[&str] = &[
40 "claude-sonnet-4.6",
41 "claude-haiku-4.5",
42 "claude-opus-4.6",
43 "claude-sonnet-4.5",
44 "claude-opus-4.5",
45 "gpt-5.4",
46 "gpt-5.4-mini",
47 "gpt-5.3-codex",
48 "gpt-5.2-codex",
49 "gpt-5.2",
50 "gpt-5.1-codex-max",
51 "gpt-5.1-codex",
52 "gpt-5.1",
53 "gpt-5",
54 "gpt-5.1-codex-mini",
55 "gpt-5-mini",
56 "gpt-4.1",
57 "gemini-3.1-pro-preview",
58 "gemini-3-pro-preview",
59];
60
61pub struct Copilot {
62 pub common: CommonAgentState,
63}
64
65pub struct CopilotLiveLogAdapter {
66 ctx: LiveLogContext,
67 session_path: Option<PathBuf>,
68 offset: u64,
69 seen_event_ids: HashSet<String>,
70}
71
72pub struct CopilotHistoricalLogAdapter;
73
74impl Copilot {
75 pub fn new() -> Self {
76 Self {
77 common: CommonAgentState::new(DEFAULT_MODEL),
78 }
79 }
80
81 async fn write_instructions_file(&self) -> Result<()> {
82 let base = self.common.get_base_path();
83 log::debug!("Writing Copilot instructions file to {}", base.display());
84 let instructions_dir = base.join(".github/instructions/agent");
85 fs::create_dir_all(&instructions_dir).await?;
86 fs::write(
87 instructions_dir.join("agent.instructions.md"),
88 &self.common.system_prompt,
89 )
90 .await?;
91 Ok(())
92 }
93
94 fn build_run_args(&self, interactive: bool, prompt: Option<&str>) -> Vec<String> {
96 let mut args = Vec::new();
97
98 if !interactive || self.common.skip_permissions {
100 args.push("--allow-all".to_string());
101 }
102
103 if !self.common.model.is_empty() {
104 args.extend(["--model".to_string(), self.common.model.clone()]);
105 }
106
107 for dir in &self.common.add_dirs {
108 args.extend(["--add-dir".to_string(), dir.clone()]);
109 }
110
111 if let Some(turns) = self.common.max_turns {
112 args.extend(["--max-turns".to_string(), turns.to_string()]);
113 }
114
115 match (interactive, prompt) {
116 (true, Some(p)) => args.extend(["-i".to_string(), p.to_string()]),
117 (false, Some(p)) => args.extend(["-p".to_string(), p.to_string()]),
118 _ => {}
119 }
120
121 args
122 }
123
124 fn make_command(&self, agent_args: Vec<String>) -> Command {
126 self.common.make_command("copilot", agent_args)
127 }
128
129 fn build_resume_args(&self, session_id: &str, prompt: &str) -> Vec<String> {
135 let mut args = vec!["--resume".to_string(), session_id.to_string()];
136 args.push("--allow-all".to_string());
137
138 if !self.common.model.is_empty() {
139 args.extend(["--model".to_string(), self.common.model.clone()]);
140 }
141
142 for dir in &self.common.add_dirs {
143 args.extend(["--add-dir".to_string(), dir.clone()]);
144 }
145
146 if let Some(turns) = self.common.max_turns {
147 args.extend(["--max-turns".to_string(), turns.to_string()]);
148 }
149
150 args.extend(["-p".to_string(), prompt.to_string()]);
151 args
152 }
153
154 async fn execute(
155 &self,
156 interactive: bool,
157 prompt: Option<&str>,
158 ) -> Result<Option<AgentOutput>> {
159 if self.common.output_format.is_some() {
161 anyhow::bail!(
162 "Copilot does not support the --output flag. Remove the flag and try again."
163 );
164 }
165
166 if !self.common.system_prompt.is_empty() {
167 log::debug!(
168 "Copilot system prompt (written to instructions): {}",
169 self.common.system_prompt
170 );
171 self.write_instructions_file().await?;
172 }
173
174 let agent_args = self.build_run_args(interactive, prompt);
175 log::debug!("Copilot command: copilot {}", agent_args.join(" "));
176 if let Some(p) = prompt {
177 log::debug!("Copilot user prompt: {p}");
178 }
179 let mut cmd = self.make_command(agent_args);
180
181 if interactive {
182 CommonAgentState::run_interactive_command_with_hook(
183 &mut cmd,
184 "Copilot",
185 self.common.on_spawn_hook.as_ref(),
186 )
187 .await?;
188 Ok(None)
189 } else {
190 self.common
191 .run_non_interactive_simple(&mut cmd, "Copilot")
192 .await
193 }
194 }
195}
196
197#[cfg(test)]
198#[path = "copilot_tests.rs"]
199mod tests;
200
201impl Default for Copilot {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207impl CopilotLiveLogAdapter {
208 pub fn new(ctx: LiveLogContext) -> Self {
209 Self {
210 ctx,
211 session_path: None,
212 offset: 0,
213 seen_event_ids: HashSet::new(),
214 }
215 }
216
217 fn discover_session_path(&self) -> Option<PathBuf> {
218 let base = copilot_session_state_dir();
219 if let Some(session_id) = &self.ctx.provider_session_id {
220 let candidate = base.join(session_id).join("events.jsonl");
221 if candidate.exists() {
222 return Some(candidate);
223 }
224 }
225
226 let started_at = system_time_from_utc(self.ctx.started_at);
227 let workspace = self
228 .ctx
229 .workspace_path
230 .as_deref()
231 .or(self.ctx.root.as_deref());
232 let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
233 let entries = std::fs::read_dir(base).ok()?;
234 for entry in entries.flatten() {
235 let path = entry.path().join("events.jsonl");
236 if !path.exists() {
237 continue;
238 }
239 let modified = entry
240 .metadata()
241 .ok()
242 .and_then(|metadata| metadata.modified().ok())
243 .or_else(|| {
244 std::fs::metadata(&path)
245 .ok()
246 .and_then(|metadata| metadata.modified().ok())
247 })?;
248 if modified < started_at {
249 continue;
250 }
251 if let Some(workspace) = workspace
252 && !copilot_session_matches_workspace(&entry.path(), workspace)
253 {
254 continue;
255 }
256 if best
257 .as_ref()
258 .map(|(current, _)| modified > *current)
259 .unwrap_or(true)
260 {
261 best = Some((modified, path));
262 }
263 }
264 best.map(|(_, path)| path)
265 }
266}
267
268#[async_trait]
269impl LiveLogAdapter for CopilotLiveLogAdapter {
270 async fn poll(&mut self, writer: &SessionLogWriter) -> Result<()> {
271 if self.session_path.is_none() {
272 self.session_path = self.discover_session_path();
273 if let Some(path) = &self.session_path {
274 writer.add_source_path(path.to_string_lossy().to_string())?;
275 let metadata_path = path.with_file_name("vscode.metadata.json");
276 if metadata_path.exists() {
277 writer.add_source_path(metadata_path.to_string_lossy().to_string())?;
278 }
279 let workspace_path = path.with_file_name("workspace.yaml");
280 if workspace_path.exists() {
281 writer.add_source_path(workspace_path.to_string_lossy().to_string())?;
282 }
283 }
284 }
285
286 let Some(path) = self.session_path.as_ref() else {
287 return Ok(());
288 };
289
290 let mut file = std::fs::File::open(path)
291 .with_context(|| format!("Failed to open {}", path.display()))?;
292 file.seek(SeekFrom::Start(self.offset))?;
293 let mut reader = BufReader::new(file);
294 let mut line = String::new();
295
296 while reader.read_line(&mut line)? > 0 {
297 self.offset += line.len() as u64;
298 let trimmed = line.trim();
299 if trimmed.is_empty() {
300 line.clear();
301 continue;
302 }
303 let Some(parsed) = parse_copilot_event_line(trimmed, &mut self.seen_event_ids) else {
304 line.clear();
305 continue;
306 };
307 if parsed.parse_failed {
308 writer.emit(
309 LogSourceKind::ProviderFile,
310 LogEventKind::ParseWarning {
311 message: "Failed to parse Copilot event line".to_string(),
312 raw: Some(trimmed.to_string()),
313 },
314 )?;
315 line.clear();
316 continue;
317 }
318 if let Some(session_id) = parsed.provider_session_id {
319 writer.set_provider_session_id(Some(session_id))?;
320 }
321 for event in parsed.events {
322 writer.emit(LogSourceKind::ProviderFile, event)?;
323 }
324 line.clear();
325 }
326
327 Ok(())
328 }
329}
330
331impl HistoricalLogAdapter for CopilotHistoricalLogAdapter {
332 fn backfill(&self, _root: Option<&str>) -> Result<Vec<BackfilledSession>> {
333 let base = copilot_session_state_dir();
334 let entries = match std::fs::read_dir(&base) {
335 Ok(entries) => entries,
336 Err(_) => return Ok(Vec::new()),
337 };
338 let mut sessions = Vec::new();
339 for entry in entries.flatten() {
340 let session_dir = entry.path();
341 if !session_dir.is_dir() {
342 continue;
343 }
344 let events_path = session_dir.join("events.jsonl");
345 if !events_path.exists() {
346 continue;
347 }
348 info!("Scanning Copilot history: {}", events_path.display());
349 let file = std::fs::File::open(&events_path)
350 .with_context(|| format!("Failed to open {}", events_path.display()))?;
351 let reader = BufReader::new(file);
352 let mut seen_event_ids = HashSet::new();
353 let mut events = Vec::new();
354 let mut provider_session_id = None;
355 let mut model = None;
356 let mut workspace_path = read_copilot_workspace_path(&session_dir);
357
358 for line in reader.lines() {
359 let line = line?;
360 let trimmed = line.trim();
361 if trimmed.is_empty() {
362 continue;
363 }
364 let Some(parsed) = parse_copilot_event_line(trimmed, &mut seen_event_ids) else {
365 continue;
366 };
367 if parsed.parse_failed {
368 events.push((
369 LogSourceKind::Backfill,
370 LogEventKind::ParseWarning {
371 message: "Failed to parse Copilot event line".to_string(),
372 raw: Some(trimmed.to_string()),
373 },
374 ));
375 continue;
376 }
377 if provider_session_id.is_none() {
378 provider_session_id = parsed.provider_session_id;
379 }
380 if model.is_none() {
381 model = parsed.model;
382 }
383 if workspace_path.is_none() {
384 workspace_path = parsed.workspace_path;
385 }
386 for event in parsed.events {
387 events.push((LogSourceKind::Backfill, event));
388 }
389 }
390
391 let session_id = provider_session_id
392 .unwrap_or_else(|| entry.file_name().to_string_lossy().to_string());
393 let mut source_paths = vec![events_path.to_string_lossy().to_string()];
394 let metadata_path = session_dir.join("vscode.metadata.json");
395 if metadata_path.exists() {
396 source_paths.push(metadata_path.to_string_lossy().to_string());
397 }
398 let workspace_yaml = session_dir.join("workspace.yaml");
399 if workspace_yaml.exists() {
400 source_paths.push(workspace_yaml.to_string_lossy().to_string());
401 }
402 sessions.push(BackfilledSession {
403 metadata: SessionLogMetadata {
404 provider: "copilot".to_string(),
405 wrapper_session_id: session_id.clone(),
406 provider_session_id: Some(session_id),
407 workspace_path,
408 command: "backfill".to_string(),
409 model,
410 resumed: false,
411 backfilled: true,
412 },
413 completeness: LogCompleteness::Full,
414 source_paths,
415 events,
416 });
417 }
418 Ok(sessions)
419 }
420}
421
422pub(crate) struct ParsedCopilotEvent {
423 pub(crate) provider_session_id: Option<String>,
424 pub(crate) model: Option<String>,
425 pub(crate) workspace_path: Option<String>,
426 pub(crate) events: Vec<LogEventKind>,
427 pub(crate) parse_failed: bool,
428}
429
430pub(crate) fn parse_copilot_event_line(
431 line: &str,
432 seen_event_ids: &mut HashSet<String>,
433) -> Option<ParsedCopilotEvent> {
434 let value: serde_json::Value = match serde_json::from_str(line) {
435 Ok(value) => value,
436 Err(_) => {
437 return Some(ParsedCopilotEvent {
438 provider_session_id: None,
439 model: None,
440 workspace_path: None,
441 events: Vec::new(),
442 parse_failed: true,
443 });
444 }
445 };
446
447 let event_id = value
448 .get("id")
449 .and_then(|value| value.as_str())
450 .unwrap_or_default();
451 if !event_id.is_empty() && !seen_event_ids.insert(event_id.to_string()) {
452 return None;
453 }
454
455 let event_type = value
456 .get("type")
457 .and_then(|value| value.as_str())
458 .unwrap_or_default();
459 let data = value
460 .get("data")
461 .cloned()
462 .unwrap_or(serde_json::Value::Null);
463 let provider_session_id = value
464 .get("data")
465 .and_then(|value| value.get("sessionId"))
466 .and_then(|value| value.as_str())
467 .map(str::to_string);
468 let model = value
469 .get("data")
470 .and_then(|value| value.get("selectedModel"))
471 .and_then(|value| value.as_str())
472 .map(str::to_string);
473 let workspace_path = value
474 .get("data")
475 .and_then(|value| value.get("context"))
476 .and_then(|value| value.get("cwd").or_else(|| value.get("gitRoot")))
477 .and_then(|value| value.as_str())
478 .map(str::to_string);
479 let mut events = Vec::new();
480
481 {
485 let cfg = crate::usage_limits::UsageLimitConfig::default();
486 if let Some(hit) = crate::providers::copilot_usage_limits::detect_json(&value, &cfg) {
487 events.push(crate::usage_limits::to_log_event_hit(hit));
488 }
489 }
490
491 match event_type {
492 "session.start" => events.push(LogEventKind::ProviderStatus {
493 message: "Copilot session started".to_string(),
494 data: Some(data),
495 }),
496 "session.model_change" => events.push(LogEventKind::ProviderStatus {
497 message: "Copilot model changed".to_string(),
498 data: Some(data),
499 }),
500 "session.info" => events.push(LogEventKind::ProviderStatus {
501 message: data
502 .get("message")
503 .and_then(|value| value.as_str())
504 .unwrap_or("Copilot session info")
505 .to_string(),
506 data: Some(data),
507 }),
508 "session.truncation" => events.push(LogEventKind::ProviderStatus {
509 message: "Copilot session truncation".to_string(),
510 data: Some(data),
511 }),
512 "user.message" => events.push(LogEventKind::UserMessage {
513 role: "user".to_string(),
514 content: data
515 .get("content")
516 .or_else(|| data.get("transformedContent"))
517 .and_then(|value| value.as_str())
518 .unwrap_or_default()
519 .to_string(),
520 message_id: value
521 .get("id")
522 .and_then(|value| value.as_str())
523 .map(str::to_string),
524 }),
525 "assistant.turn_start" => events.push(LogEventKind::ProviderStatus {
526 message: "Copilot assistant turn started".to_string(),
527 data: Some(data),
528 }),
529 "assistant.turn_end" => events.push(LogEventKind::ProviderStatus {
530 message: "Copilot assistant turn ended".to_string(),
531 data: Some(data),
532 }),
533 "assistant.message" => {
534 let message_id = data
535 .get("messageId")
536 .and_then(|value| value.as_str())
537 .map(str::to_string);
538 let content = data
539 .get("content")
540 .and_then(|value| value.as_str())
541 .unwrap_or_default()
542 .to_string();
543 if !content.is_empty() {
544 events.push(LogEventKind::AssistantMessage {
545 content,
546 message_id: message_id.clone(),
547 });
548 }
549 if let Some(tool_requests) = data.get("toolRequests").and_then(|value| value.as_array())
550 {
551 for request in tool_requests {
552 let name = request
553 .get("name")
554 .and_then(|value| value.as_str())
555 .unwrap_or_default();
556 events.push(LogEventKind::ToolCall {
557 tool_kind: Some(tool_kind_from_name(name)),
558 tool_name: name.to_string(),
559 tool_id: request
560 .get("toolCallId")
561 .and_then(|value| value.as_str())
562 .map(str::to_string),
563 input: request.get("arguments").cloned(),
564 });
565 }
566 }
567 }
568 "assistant.reasoning" => {
569 let content = data
570 .get("content")
571 .and_then(|value| value.as_str())
572 .unwrap_or_default()
573 .to_string();
574 if !content.is_empty() {
575 events.push(LogEventKind::Reasoning {
576 content,
577 message_id: data
578 .get("reasoningId")
579 .and_then(|value| value.as_str())
580 .map(str::to_string),
581 });
582 }
583 }
584 "tool.execution_start" => {
585 let name = data
586 .get("toolName")
587 .and_then(|value| value.as_str())
588 .unwrap_or_default();
589 events.push(LogEventKind::ToolCall {
590 tool_kind: Some(tool_kind_from_name(name)),
591 tool_name: name.to_string(),
592 tool_id: data
593 .get("toolCallId")
594 .and_then(|value| value.as_str())
595 .map(str::to_string),
596 input: data.get("arguments").cloned(),
597 });
598 }
599 "tool.execution_complete" => {
600 let name = data.get("toolName").and_then(|value| value.as_str());
601 events.push(LogEventKind::ToolResult {
602 tool_kind: name.map(tool_kind_from_name),
603 tool_name: name.map(str::to_string),
604 tool_id: data
605 .get("toolCallId")
606 .and_then(|value| value.as_str())
607 .map(str::to_string),
608 success: data.get("success").and_then(|value| value.as_bool()),
609 output: data
610 .get("result")
611 .and_then(|value| value.get("content"))
612 .and_then(|value| value.as_str())
613 .map(str::to_string),
614 error: data
615 .get("result")
616 .and_then(|value| value.get("error"))
617 .and_then(|value| value.as_str())
618 .map(str::to_string),
619 data: Some(data),
620 });
621 }
622 _ => events.push(LogEventKind::ProviderStatus {
623 message: format!("Copilot event: {event_type}"),
624 data: Some(data),
625 }),
626 }
627
628 Some(ParsedCopilotEvent {
629 provider_session_id,
630 model,
631 workspace_path,
632 events,
633 parse_failed: false,
634 })
635}
636
637fn copilot_session_state_dir() -> PathBuf {
638 session_state_dir()
639}
640
641fn read_copilot_workspace_path(session_dir: &Path) -> Option<String> {
642 let metadata_path = session_dir.join("vscode.metadata.json");
643 if let Ok(content) = std::fs::read_to_string(&metadata_path)
644 && let Ok(value) = serde_json::from_str::<serde_json::Value>(&content)
645 {
646 if let Some(path) = value
647 .get("cwd")
648 .or_else(|| value.get("workspacePath"))
649 .or_else(|| value.get("gitRoot"))
650 .and_then(|value| value.as_str())
651 {
652 return Some(path.to_string());
653 }
654 }
655 let workspace_yaml = session_dir.join("workspace.yaml");
656 if let Ok(content) = std::fs::read_to_string(workspace_yaml) {
657 for line in content.lines() {
658 let trimmed = line.trim();
659 if let Some(rest) = trimmed
660 .strip_prefix("cwd:")
661 .or_else(|| trimmed.strip_prefix("workspace:"))
662 .or_else(|| trimmed.strip_prefix("path:"))
663 {
664 return Some(rest.trim().trim_matches('"').to_string());
665 }
666 }
667 }
668 None
669}
670
671fn copilot_session_matches_workspace(session_dir: &Path, workspace: &str) -> bool {
672 if let Some(candidate) = read_copilot_workspace_path(session_dir) {
673 return candidate == workspace;
674 }
675
676 let events_path = session_dir.join("events.jsonl");
677 let file = match std::fs::File::open(events_path) {
678 Ok(file) => file,
679 Err(_) => return false,
680 };
681 let reader = BufReader::new(file);
682 for line in reader.lines().map_while(Result::ok).take(8) {
683 let Ok(value) = serde_json::from_str::<serde_json::Value>(&line) else {
684 continue;
685 };
686 let Some(data) = value.get("data") else {
687 continue;
688 };
689 let candidate = data
690 .get("context")
691 .and_then(|context| context.get("cwd").or_else(|| context.get("gitRoot")))
692 .and_then(|value| value.as_str());
693 if candidate == Some(workspace) {
694 return true;
695 }
696 }
697 false
698}
699
700fn system_time_from_utc(value: chrono::DateTime<chrono::Utc>) -> std::time::SystemTime {
701 std::time::SystemTime::UNIX_EPOCH
702 + std::time::Duration::from_secs(value.timestamp().max(0) as u64)
703}
704
705#[async_trait]
706impl Agent for Copilot {
707 fn name(&self) -> &str {
708 "copilot"
709 }
710
711 fn default_model() -> &'static str {
712 DEFAULT_MODEL
713 }
714
715 fn model_for_size(size: ModelSize) -> &'static str {
716 match size {
717 ModelSize::Small => "claude-haiku-4.5",
718 ModelSize::Medium => "claude-sonnet-4.6",
719 ModelSize::Large => "claude-opus-4.6",
720 }
721 }
722
723 fn available_models() -> &'static [&'static str] {
724 AVAILABLE_MODELS
725 }
726
727 crate::providers::common::impl_common_agent_setters!();
728
729 fn set_skip_permissions(&mut self, skip: bool) {
730 self.common.skip_permissions = skip;
731 }
732
733 crate::providers::common::impl_as_any!();
734
735 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
736 self.execute(false, prompt).await
737 }
738
739 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
740 self.execute(true, prompt).await?;
741 Ok(())
742 }
743
744 async fn run_resume(&self, session_id: Option<&str>, last: bool) -> Result<()> {
745 let mut args = if let Some(session_id) = session_id {
746 vec!["--resume".to_string(), session_id.to_string()]
747 } else if last {
748 vec!["--continue".to_string()]
749 } else {
750 vec!["--resume".to_string()]
751 };
752
753 if self.common.skip_permissions {
754 args.push("--allow-all".to_string());
755 }
756
757 if !self.common.model.is_empty() {
758 args.extend(["--model".to_string(), self.common.model.clone()]);
759 }
760
761 for dir in &self.common.add_dirs {
762 args.extend(["--add-dir".to_string(), dir.clone()]);
763 }
764
765 let mut cmd = self.make_command(args);
766 CommonAgentState::run_interactive_command_with_hook(
767 &mut cmd,
768 "Copilot",
769 self.common.on_spawn_hook.as_ref(),
770 )
771 .await
772 }
773
774 async fn run_resume_with_prompt(
775 &self,
776 session_id: &str,
777 prompt: &str,
778 ) -> Result<Option<AgentOutput>> {
779 log::debug!("Copilot resume with prompt: session={session_id}, prompt={prompt}");
780
781 if self.common.output_format.is_some() {
782 anyhow::bail!(
783 "Copilot does not support the --output flag. Remove the flag and try again."
784 );
785 }
786
787 if !self.common.system_prompt.is_empty() {
788 self.write_instructions_file().await?;
789 }
790
791 let args = self.build_resume_args(session_id, prompt);
792 let mut cmd = self.make_command(args);
793 self.common
794 .run_non_interactive_simple(&mut cmd, "Copilot")
795 .await
796 }
797
798 async fn cleanup(&self) -> Result<()> {
799 log::debug!("Cleaning up Copilot agent resources");
800 let base = self.common.get_base_path();
801 let instructions_file = base.join(".github/instructions/agent/agent.instructions.md");
802
803 if instructions_file.exists() {
804 fs::remove_file(&instructions_file).await?;
805 }
806
807 let agent_dir = base.join(".github/instructions/agent");
809 if agent_dir.exists()
810 && fs::read_dir(&agent_dir)
811 .await?
812 .next_entry()
813 .await?
814 .is_none()
815 {
816 fs::remove_dir(&agent_dir).await?;
817 }
818
819 let instructions_dir = base.join(".github/instructions");
820 if instructions_dir.exists()
821 && fs::read_dir(&instructions_dir)
822 .await?
823 .next_entry()
824 .await?
825 .is_none()
826 {
827 fs::remove_dir(&instructions_dir).await?;
828 }
829
830 let github_dir = base.join(".github");
831 if github_dir.exists()
832 && fs::read_dir(&github_dir)
833 .await?
834 .next_entry()
835 .await?
836 .is_none()
837 {
838 fs::remove_dir(&github_dir).await?;
839 }
840
841 Ok(())
842 }
843}