1use super::dto::DesktopSessionItem;
4use crate::daemon::LifecycleReadOptions;
5use crate::lifecycle_store::{LifecycleStore, lifecycle_root_from_config};
6use crate::session_sources::{load_provider_sessions, raw_session_id};
7use std::fs;
8use std::path::Path;
9
10use super::dto::DesktopDaemonRequest;
11
12pub fn parse_file_list(value: &str) -> Vec<String> {
13 value
14 .split([',', '\n'])
15 .map(str::trim)
16 .filter(|item| !item.is_empty())
17 .map(ToString::to_string)
18 .collect()
19}
20
21pub fn parse_csv_items(value: &str) -> Vec<String> {
22 value
23 .split(',')
24 .map(str::trim)
25 .filter(|item| !item.is_empty())
26 .map(ToString::to_string)
27 .collect()
28}
29
30pub(super) fn lifecycle_read_options(
31 daemon: Option<&DesktopDaemonRequest>,
32) -> LifecycleReadOptions {
33 match daemon {
34 Some(DesktopDaemonRequest {
35 enabled: true,
36 daemon_bin: Some(daemon_bin),
37 }) => LifecycleReadOptions::with_daemon(daemon_bin.as_path()),
38 _ => LifecycleReadOptions::default(),
39 }
40}
41
42pub(super) fn store_from_config_path(config_path: &Path) -> LifecycleStore {
43 let config_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
44 LifecycleStore::new(lifecycle_root_from_config(config_dir).as_path())
45}
46
47pub(super) fn load_session_index(_config_path: &Path) -> anyhow::Result<Vec<DesktopSessionItem>> {
48 let mut sessions = load_provider_sessions(None)?;
49 sessions.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
50 Ok(sessions)
51}
52
53pub(super) fn load_session_index_filtered(
54 _config_path: &Path,
55 provider_filter: Option<&str>,
56) -> anyhow::Result<Vec<DesktopSessionItem>> {
57 let mut sessions = load_provider_sessions(provider_filter)?;
58 sessions.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
59 Ok(sessions)
60}
61
62pub(super) fn build_continue_command(session: &DesktopSessionItem) -> Option<String> {
63 let cwd_prefix = session
64 .cwd
65 .as_deref()
66 .map(shell_cd_prefix)
67 .unwrap_or_default();
68 let raw_id = raw_session_id(&session.session_id);
69 match session.provider.as_str() {
70 "claude" => Some(format!("{cwd_prefix}claude -r \"{raw_id}\"")),
71 "codex" => Some(format!("{cwd_prefix}codex resume \"{raw_id}\"")),
72 _ => None,
73 }
74}
75
76pub(super) fn delete_session_file(session: &DesktopSessionItem) -> anyhow::Result<()> {
77 let Some(path) = session.source_path.as_deref() else {
78 anyhow::bail!("当前会话没有可删除的本地文件")
79 };
80 let file_path = Path::new(path);
81 if !file_path.exists() {
82 anyhow::bail!("本地会话文件不存在:{}", file_path.display())
83 }
84 if !file_path.is_file() {
85 anyhow::bail!("本地会话路径不是文件:{}", file_path.display())
86 }
87 fs::remove_file(file_path)?;
88 Ok(())
89}
90
91fn shell_cd_prefix(cwd: &str) -> String {
92 format!("cd '{}' && ", cwd.replace('\'', "'\\''"))
93}
94
95pub(super) fn launch_terminal_command(command: &str) -> anyhow::Result<()> {
96 #[cfg(target_os = "macos")]
97 {
98 let script = format!(
99 "tell application \"Terminal\"\nactivate\ndo script \"{}\"\nend tell",
100 command.replace('\\', "\\\\").replace('"', "\\\"")
101 );
102 let output = std::process::Command::new("osascript")
103 .arg("-e")
104 .arg(script)
105 .output()?;
106 if !output.status.success() {
107 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
108 let detail = if stderr.is_empty() {
109 format!("osascript exited with status {}", output.status)
110 } else {
111 stderr
112 };
113 anyhow::bail!("无法通过 Terminal 启动命令 `{command}`:{detail}");
114 }
115 Ok(())
116 }
117
118 #[cfg(not(target_os = "macos"))]
119 {
120 let _ = command;
121 anyhow::bail!("continue session is only implemented for macOS right now")
122 }
123}